From cb7d9364e5a824579dabafc9ceb24729c9007277 Mon Sep 17 00:00:00 2001 From: woodser Date: Mon, 14 Aug 2023 08:19:03 -0400 Subject: [PATCH] check trades, disputes, and offers and add prompt on shut down Co-authored-by: jmacxx <47253594+jmacxx@users.noreply.github.com> --- .../haveno/core/api/CoreDisputesService.java | 4 - .../haveno/core/support/SupportManager.java | 4 +- .../haveno/core/support/dispute/Dispute.java | 14 -- .../core/support/dispute/DisputeManager.java | 47 +++++-- .../support/dispute/DisputeValidation.java | 124 ------------------ .../support/traderchat/TraderChatManager.java | 10 +- .../java/haveno/core/trade/TradeManager.java | 6 +- .../resources/i18n/displayStrings.properties | 3 + .../witness/AccountAgeWitnessServiceTest.java | 2 - .../java/haveno/desktop/app/HavenoApp.java | 107 +++++++++++---- .../main/overlays/windows/ContractWindow.java | 2 - .../pendingtrades/PendingTradesDataModel.java | 4 - .../main/support/dispute/DisputeView.java | 3 - proto/src/main/proto/pb.proto | 42 +++--- 14 files changed, 155 insertions(+), 217 deletions(-) diff --git a/core/src/main/java/haveno/core/api/CoreDisputesService.java b/core/src/main/java/haveno/core/api/CoreDisputesService.java index 912cdaa4d2..7369fbd0ab 100644 --- a/core/src/main/java/haveno/core/api/CoreDisputesService.java +++ b/core/src/main/java/haveno/core/api/CoreDisputesService.java @@ -105,8 +105,6 @@ public class CoreDisputesService { PubKeyRing arbitratorPubKeyRing = trade.getArbitrator().getPubKeyRing(); checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null"); - byte[] depositTxSerialized = null; // depositTx.bitcoinSerialize(); TODO (woodser) - String depositTxHashAsString = null; // depositTx.getHashAsString(); TODO (woodser) Dispute dispute = new Dispute(new Date().getTime(), trade.getId(), pubKey.hashCode(), // trader id, @@ -118,9 +116,7 @@ public class CoreDisputesService { trade.getMaxTradePeriodDate().getTime(), trade.getContract(), trade.getContractHash(), - depositTxSerialized, payoutTxSerialized, - depositTxHashAsString, payoutTxHashAsString, trade.getContractAsJson(), trade.getMaker().getContractSignature(), diff --git a/core/src/main/java/haveno/core/support/SupportManager.java b/core/src/main/java/haveno/core/support/SupportManager.java index 53177dbdf3..783e00c68d 100644 --- a/core/src/main/java/haveno/core/support/SupportManager.java +++ b/core/src/main/java/haveno/core/support/SupportManager.java @@ -114,7 +114,7 @@ public abstract class SupportManager { public abstract boolean channelOpen(ChatMessage message); - public abstract List getAllChatMessages(); + public abstract List getAllChatMessages(String tradeId); public abstract void addAndPersistChatMessage(ChatMessage message); @@ -204,7 +204,7 @@ public abstract class SupportManager { ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage()); } - getAllChatMessages().stream() + getAllChatMessages(ackMessage.getSourceId()).stream() .filter(msg -> msg.getUid().equals(ackMessage.getSourceUid())) .forEach(msg -> { if (ackMessage.isSuccess()) diff --git a/core/src/main/java/haveno/core/support/dispute/Dispute.java b/core/src/main/java/haveno/core/support/dispute/Dispute.java index 287b94adc9..cabd830ba6 100644 --- a/core/src/main/java/haveno/core/support/dispute/Dispute.java +++ b/core/src/main/java/haveno/core/support/dispute/Dispute.java @@ -92,12 +92,8 @@ public final class Dispute implements NetworkPayload, PersistablePayload { @Nullable private final byte[] contractHash; @Nullable - private final byte[] depositTxSerialized; - @Nullable private final byte[] payoutTxSerialized; @Nullable - private final String depositTxId; - @Nullable private final String payoutTxId; private String contractAsJson; @Nullable @@ -171,9 +167,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload { long tradePeriodEnd, Contract contract, @Nullable byte[] contractHash, - @Nullable byte[] depositTxSerialized, @Nullable byte[] payoutTxSerialized, - @Nullable String depositTxId, @Nullable String payoutTxId, String contractAsJson, @Nullable byte[] makerContractSignature, @@ -194,9 +188,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload { this.tradePeriodEnd = tradePeriodEnd; this.contract = contract; this.contractHash = contractHash; - this.depositTxSerialized = depositTxSerialized; this.payoutTxSerialized = payoutTxSerialized; - this.depositTxId = depositTxId; this.payoutTxId = payoutTxId; this.contractAsJson = contractAsJson; this.makerContractSignature = makerContractSignature; @@ -243,9 +235,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload { .setId(id); Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(e))); - Optional.ofNullable(depositTxSerialized).ifPresent(e -> builder.setDepositTxSerialized(ByteString.copyFrom(e))); Optional.ofNullable(payoutTxSerialized).ifPresent(e -> builder.setPayoutTxSerialized(ByteString.copyFrom(e))); - Optional.ofNullable(depositTxId).ifPresent(builder::setDepositTxId); Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId); Optional.ofNullable(disputePayoutTxId).ifPresent(builder::setDisputePayoutTxId); Optional.ofNullable(makerContractSignature).ifPresent(e -> builder.setMakerContractSignature(ByteString.copyFrom(e))); @@ -273,9 +263,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload { proto.getTradePeriodEnd(), Contract.fromProto(proto.getContract(), coreProtoResolver), ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()), - ProtoUtil.byteArrayOrNullFromProto(proto.getDepositTxSerialized()), ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSerialized()), - ProtoUtil.stringOrNullFromProto(proto.getDepositTxId()), ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId()), proto.getContractAsJson(), ProtoUtil.byteArrayOrNullFromProto(proto.getMakerContractSignature()), @@ -516,9 +504,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload { ",\n tradePeriodEnd=" + tradePeriodEnd + ",\n contract=" + contract + ",\n contractHash=" + Utilities.bytesAsHexString(contractHash) + - ",\n depositTxSerialized=" + Utilities.bytesAsHexString(depositTxSerialized) + ",\n payoutTxSerialized=" + Utilities.bytesAsHexString(payoutTxSerialized) + - ",\n depositTxId='" + depositTxId + '\'' + ",\n payoutTxId='" + payoutTxId + '\'' + ",\n contractAsJson='" + contractAsJson + '\'' + ",\n makerContractSignature='" + Utilities.bytesAsHexString(makerContractSignature) + '\'' + diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java index a00573c658..a125fa2e8e 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeManager.java @@ -91,6 +91,7 @@ public abstract class DisputeManager> extends Sup protected final DisputeListService disputeListService; private final Config config; private final PriceFeedService priceFeedService; + protected String pendingOutgoingMessage; @Getter protected final ObservableList validationExceptions = @@ -122,6 +123,7 @@ public abstract class DisputeManager> extends Sup this.disputeListService = disputeListService; this.config = config; this.priceFeedService = priceFeedService; + clearPendingMessage(); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -140,7 +142,7 @@ public abstract class DisputeManager> extends Sup @Override public NodeAddress getPeerNodeAddress(ChatMessage message) { Optional disputeOptional = findDispute(message); - if (!disputeOptional.isPresent()) { + if (disputeOptional.isEmpty()) { log.warn("Could not find dispute for tradeId = {} traderId = {}", message.getTradeId(), message.getTraderId()); return null; @@ -151,7 +153,7 @@ public abstract class DisputeManager> extends Sup @Override public PubKeyRing getPeerPubKeyRing(ChatMessage message) { Optional disputeOptional = findDispute(message); - if (!disputeOptional.isPresent()) { + if (disputeOptional.isEmpty()) { log.warn("Could not find dispute for tradeId = {} traderId = {}", message.getTradeId(), message.getTraderId()); return null; @@ -161,12 +163,11 @@ public abstract class DisputeManager> extends Sup } @Override - public List getAllChatMessages() { - synchronized (getDisputeList()) { - return getDisputeList().stream() - .flatMap(dispute -> dispute.getChatMessages().stream()) - .collect(Collectors.toList()); - } + public List getAllChatMessages(String tradeId) { + return getDisputeList().stream() + .filter(dispute -> dispute.getTradeId().equals(tradeId)) + .flatMap(dispute -> dispute.getChatMessages().stream()) + .collect(Collectors.toList()); } @Override @@ -369,6 +370,7 @@ public abstract class DisputeManager> extends Sup disputeOpenedMessage.getClass().getSimpleName(), agentNodeAddress, disputeOpenedMessage.getTradeId(), disputeOpenedMessage.getUid(), chatMessage.getUid()); + recordPendingMessage(disputeOpenedMessage.getClass().getSimpleName()); mailboxMessageService.sendEncryptedMailboxMessage(agentNodeAddress, dispute.getAgentPubKeyRing(), disputeOpenedMessage, @@ -380,6 +382,7 @@ public abstract class DisputeManager> extends Sup 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 @@ -396,6 +399,7 @@ public abstract class DisputeManager> extends Sup 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 @@ -413,6 +417,7 @@ public abstract class DisputeManager> extends Sup 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); @@ -586,9 +591,7 @@ public abstract class DisputeManager> extends Sup disputeFromOpener.getTradePeriodEnd().getTime(), contractFromOpener, disputeFromOpener.getContractHash(), - disputeFromOpener.getDepositTxSerialized(), disputeFromOpener.getPayoutTxSerialized(), - disputeFromOpener.getDepositTxId(), disputeFromOpener.getPayoutTxId(), disputeFromOpener.getContractAsJson(), disputeFromOpener.getMakerContractSignature(), @@ -653,6 +656,7 @@ public abstract class DisputeManager> extends Sup peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), chatMessage.getUid()); + recordPendingMessage(peerOpenedDisputeMessage.getClass().getSimpleName()); mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress, peersPubKeyRing, peerOpenedDisputeMessage, @@ -665,6 +669,7 @@ public abstract class DisputeManager> extends Sup peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), chatMessage.getUid()); + clearPendingMessage(); // 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); @@ -679,6 +684,7 @@ public abstract class DisputeManager> extends Sup peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), chatMessage.getUid()); + clearPendingMessage(); // 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); @@ -693,6 +699,7 @@ public abstract class DisputeManager> extends Sup peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), chatMessage.getUid(), errorMessage); + clearPendingMessage(); // 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); @@ -749,6 +756,7 @@ public abstract class DisputeManager> extends Sup disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), disputeClosedMessage.getClass().getSimpleName(), disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), disputeResult.getChatMessage().getUid()); + recordPendingMessage(disputeClosedMessage.getClass().getSimpleName()); mailboxMessageService.sendEncryptedMailboxMessage(receiver.getNodeAddress(), dispute.getTraderPubKeyRing(), disputeClosedMessage, @@ -761,6 +769,7 @@ public abstract class DisputeManager> extends Sup disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), disputeResult.getChatMessage().getUid()); + clearPendingMessage(); // We use the chatMessage wrapped inside the DisputeClosedMessage for // the state, as that is displayed to the user and we only persist that msg disputeResult.getChatMessage().setArrived(true); @@ -778,6 +787,7 @@ public abstract class DisputeManager> extends Sup disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), disputeResult.getChatMessage().getUid()); + clearPendingMessage(); // We use the chatMessage wrapped inside the DisputeClosedMessage for // the state, as that is displayed to the user and we only persist that msg disputeResult.getChatMessage().setStoredInMailbox(true); @@ -795,6 +805,7 @@ public abstract class DisputeManager> extends Sup disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), disputeResult.getChatMessage().getUid(), errorMessage); + clearPendingMessage(); // We use the chatMessage wrapped inside the DisputeClosedMessage for // the state, as that is displayed to the user and we only persist that msg disputeResult.getChatMessage().setSendMessageError(errorMessage); @@ -1091,4 +1102,20 @@ public abstract class DisputeManager> extends Sup return null; } } + + public boolean hasPendingMessageAtShutdown() { + if (pendingOutgoingMessage.length() > 0) { + log.warn("{} has an outgoing message pending: {}", this.getClass().getSimpleName(), pendingOutgoingMessage); + return true; + } + return false; + } + + private void recordPendingMessage(String className) { + pendingOutgoingMessage = className; + } + + private void clearPendingMessage() { + pendingOutgoingMessage = ""; + } } diff --git a/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java b/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java index ed2900e570..fb3cc6a3a8 100644 --- a/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java +++ b/core/src/main/java/haveno/core/support/dispute/DisputeValidation.java @@ -19,8 +19,6 @@ package haveno.core.support.dispute; import haveno.common.config.Config; import haveno.common.crypto.Hash; -import haveno.common.util.Tuple3; -import haveno.core.support.SupportType; import haveno.core.trade.Contract; import haveno.core.trade.HavenoUtils; import haveno.core.trade.Trade; @@ -35,13 +33,7 @@ import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionOutput; import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.Set; -import java.util.function.Consumer; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @@ -131,122 +123,6 @@ public class DisputeValidation { "; dispute.getDonationAddressOfDelayedPayoutTx()=" + dispute.getDonationAddressOfDelayedPayoutTx()); } - public static void testIfAnyDisputeTriedReplay(List disputeList, - Consumer exceptionHandler) { - var tuple = getTestReplayHashMaps(disputeList); - Map> disputesPerTradeId = tuple.first; - Map> disputesPerDelayedPayoutTxId = tuple.second; - Map> disputesPerDepositTxId = tuple.third; - - disputeList.forEach(disputeToTest -> { - try { - testIfDisputeTriesReplay(disputeToTest, - disputesPerTradeId, - disputesPerDelayedPayoutTxId, - disputesPerDepositTxId); - - } catch (DisputeReplayException e) { - exceptionHandler.accept(e); - } - }); - } - - public static void testIfDisputeTriesReplay(Dispute dispute, - List disputeList) throws DisputeReplayException { - var tuple = getTestReplayHashMaps(disputeList); - Map> disputesPerTradeId = tuple.first; - Map> disputesPerDelayedPayoutTxId = tuple.second; - Map> disputesPerDepositTxId = tuple.third; - - testIfDisputeTriesReplay(dispute, - disputesPerTradeId, - disputesPerDelayedPayoutTxId, - disputesPerDepositTxId); - } - - private static Tuple3>, Map>, Map>> getTestReplayHashMaps( - List disputeList) { - Map> disputesPerTradeId = new HashMap<>(); - Map> disputesPerDelayedPayoutTxId = new HashMap<>(); - Map> disputesPerDepositTxId = new HashMap<>(); - disputeList.forEach(dispute -> { - String uid = dispute.getUid(); - - String tradeId = dispute.getTradeId(); - disputesPerTradeId.putIfAbsent(tradeId, new HashSet<>()); - Set set = disputesPerTradeId.get(tradeId); - set.add(uid); - - String delayedPayoutTxId = dispute.getDelayedPayoutTxId(); - if (delayedPayoutTxId != null) { - disputesPerDelayedPayoutTxId.putIfAbsent(delayedPayoutTxId, new HashSet<>()); - set = disputesPerDelayedPayoutTxId.get(delayedPayoutTxId); - set.add(uid); - } - - String depositTxId = dispute.getDepositTxId(); - if (depositTxId != null) { - disputesPerDepositTxId.putIfAbsent(depositTxId, new HashSet<>()); - set = disputesPerDepositTxId.get(depositTxId); - set.add(uid); - } - }); - - return new Tuple3<>(disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId); - } - - private static void testIfDisputeTriesReplay(Dispute disputeToTest, - Map> disputesPerTradeId, - Map> disputesPerDelayedPayoutTxId, - Map> disputesPerDepositTxId) - throws DisputeReplayException { - try { - String disputeToTestTradeId = disputeToTest.getTradeId(); - String disputeToTestDelayedPayoutTxId = disputeToTest.getDelayedPayoutTxId(); - String disputeToTestDepositTxId = disputeToTest.getDepositTxId(); - String disputeToTestUid = disputeToTest.getUid(); - - // For pre v1.4.0 we do not get the delayed payout tx sent in mediation cases but in refund agent case we do. - // So until all users have updated to 1.4.0 we only check in refund agent case. With 1.4.0 we send the - // delayed payout tx also in mediation cases and that if check can be removed. - if (disputeToTest.getSupportType() == SupportType.REFUND) { - checkNotNull(disputeToTestDelayedPayoutTxId, - "Delayed payout transaction ID is null. " + - "Trade ID: " + disputeToTestTradeId); - } - checkNotNull(disputeToTestDepositTxId, - "depositTxId must not be null. Trade ID: " + disputeToTestTradeId); - checkNotNull(disputeToTestUid, - "agentsUid must not be null. Trade ID: " + disputeToTestTradeId); - - Set disputesPerTradeIdItems = disputesPerTradeId.get(disputeToTestTradeId); - checkArgument(disputesPerTradeIdItems != null && disputesPerTradeIdItems.size() <= 2, - "We found more then 2 disputes with the same trade ID. " + - "Trade ID: " + disputeToTestTradeId); - if (!disputesPerDelayedPayoutTxId.isEmpty()) { - Set disputesPerDelayedPayoutTxIdItems = disputesPerDelayedPayoutTxId.get(disputeToTestDelayedPayoutTxId); - checkArgument(disputesPerDelayedPayoutTxIdItems != null && disputesPerDelayedPayoutTxIdItems.size() <= 2, - "We found more then 2 disputes with the same delayedPayoutTxId. " + - "Trade ID: " + disputeToTestTradeId); - } - if (!disputesPerDepositTxId.isEmpty()) { - Set disputesPerDepositTxIdItems = disputesPerDepositTxId.get(disputeToTestDepositTxId); - checkArgument(disputesPerDepositTxIdItems != null && disputesPerDepositTxIdItems.size() <= 2, - "We found more then 2 disputes with the same depositTxId. " + - "Trade ID: " + disputeToTestTradeId); - } - } catch (IllegalArgumentException e) { - throw new DisputeReplayException(disputeToTest, e.getMessage()); - } catch (NullPointerException e) { - log.error("NullPointerException at testIfDisputeTriesReplay: " + - "disputeToTest={}, disputesPerTradeId={}, disputesPerDelayedPayoutTxId={}, " + - "disputesPerDepositTxId={}", - disputeToTest, disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId); - throw new DisputeReplayException(disputeToTest, e.toString() + " at dispute " + disputeToTest.toString()); - } - } - - /////////////////////////////////////////////////////////////////////////////////////////// // Exceptions /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/haveno/core/support/traderchat/TraderChatManager.java b/core/src/main/java/haveno/core/support/traderchat/TraderChatManager.java index 048f529a64..eea25bc6d4 100644 --- a/core/src/main/java/haveno/core/support/traderchat/TraderChatManager.java +++ b/core/src/main/java/haveno/core/support/traderchat/TraderChatManager.java @@ -31,13 +31,14 @@ import haveno.core.trade.TradeManager; import haveno.network.p2p.AckMessageSourceType; import haveno.network.p2p.NodeAddress; import haveno.network.p2p.P2PService; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; import lombok.extern.slf4j.Slf4j; import javax.inject.Inject; import javax.inject.Singleton; import java.util.List; -import java.util.stream.Collectors; +import java.util.Optional; @Slf4j @Singleton @@ -97,10 +98,9 @@ public class TraderChatManager extends SupportManager { } @Override - public List getAllChatMessages() { - return tradeManager.getObservableList().stream() - .flatMap(trade -> trade.getChatMessages().stream()) - .collect(Collectors.toList()); + public List getAllChatMessages(String tradeId) { + return Optional.of(tradeManager.getTrade(tradeId)).map(Trade::getChatMessages) + .orElse(FXCollections.emptyObservableList()); } @Override diff --git a/core/src/main/java/haveno/core/trade/TradeManager.java b/core/src/main/java/haveno/core/trade/TradeManager.java index c305a486f2..c0210b414b 100644 --- a/core/src/main/java/haveno/core/trade/TradeManager.java +++ b/core/src/main/java/haveno/core/trade/TradeManager.java @@ -1137,7 +1137,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi // TODO (woodser): make Optional versus Trade return types consistent public Trade getTrade(String tradeId) { - return getOpenTrade(tradeId).orElseGet(() -> getClosedTrade(tradeId).orElseGet(() -> null)); + return getOpenTrade(tradeId).orElseGet(() -> getClosedTrade(tradeId).orElseGet(() -> getFailedTrade(tradeId).orElseGet(() -> null))); } public Optional getOpenTrade(String tradeId) { @@ -1176,6 +1176,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi public Optional getClosedTrade(String tradeId) { return closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(tradeId)).findFirst(); } + + public Optional getFailedTrade(String tradeId) { + return failedTradesManager.getTradeById(tradeId); + } private void addTrade(Trade trade) { synchronized (tradableList) { diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 85c6d17d6f..356768eaac 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2159,6 +2159,9 @@ popup.info.shutDownWithOpenOffers=Haveno is being shut down, but there are open (i.e., make sure it doesn't go into standby mode...monitor standby is not a problem). popup.info.shutDownWithTradeInit={0}\n\ This trade has not finished initializing; shutting down now will probably make it corrupted. Please wait a minute and try again. +popup.info.shutDownWithDisputeInit=Haveno is being shut down, but there is a Dispute system message still pending.\n\ + Please wait a minute before shutting down. +popup.info.shutDownQuery=Are you sure you want to exit Haveno? popup.info.qubesOSSetupInfo=It appears you are running Haveno on Qubes OS. \n\n\ Please make sure your Haveno qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Haveno_on_Qubes]. popup.info.p2pStatusIndicator.red={0}\n\n\ diff --git a/core/src/test/java/haveno/core/account/witness/AccountAgeWitnessServiceTest.java b/core/src/test/java/haveno/core/account/witness/AccountAgeWitnessServiceTest.java index 0198148855..2dd0279432 100644 --- a/core/src/test/java/haveno/core/account/witness/AccountAgeWitnessServiceTest.java +++ b/core/src/test/java/haveno/core/account/witness/AccountAgeWitnessServiceTest.java @@ -178,8 +178,6 @@ public class AccountAgeWitnessServiceTest { null, null, null, - null, - null, "contractAsJson", null, null, diff --git a/desktop/src/main/java/haveno/desktop/app/HavenoApp.java b/desktop/src/main/java/haveno/desktop/app/HavenoApp.java index d8c03a320f..d2d8d60c37 100644 --- a/desktop/src/main/java/haveno/desktop/app/HavenoApp.java +++ b/desktop/src/main/java/haveno/desktop/app/HavenoApp.java @@ -17,6 +17,11 @@ package haveno.desktop.app; +import static haveno.desktop.util.Layout.INITIAL_WINDOW_HEIGHT; +import static haveno.desktop.util.Layout.INITIAL_WINDOW_WIDTH; +import static haveno.desktop.util.Layout.MIN_WINDOW_HEIGHT; +import static haveno.desktop.util.Layout.MIN_WINDOW_WIDTH; + import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; import com.google.common.base.Joiner; @@ -26,12 +31,18 @@ import com.google.inject.name.Names; import haveno.common.app.DevEnv; import haveno.common.app.Log; import haveno.common.config.Config; +import haveno.common.crypto.Hash; import haveno.common.setup.GracefulShutDownHandler; import haveno.common.setup.UncaughtExceptionHandler; import haveno.common.util.Utilities; import haveno.core.locale.Res; import haveno.core.offer.OpenOffer; import haveno.core.offer.OpenOfferManager; +import haveno.core.support.dispute.arbitration.ArbitrationManager; +import haveno.core.support.dispute.mediation.MediationManager; +import haveno.core.support.dispute.refund.RefundManager; +import haveno.core.trade.Trade; +import haveno.core.trade.TradeManager; import haveno.core.user.Cookie; import haveno.core.user.CookieKey; import haveno.core.user.Preferences; @@ -47,7 +58,15 @@ import haveno.desktop.main.overlays.windows.FilterWindow; import haveno.desktop.main.overlays.windows.SendAlertMessageWindow; import haveno.desktop.main.overlays.windows.ShowWalletDataWindow; import haveno.desktop.util.CssTheme; +import haveno.desktop.util.DisplayUtils; import haveno.desktop.util.ImageUtil; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import javafx.application.Application; import javafx.geometry.BoundingBox; import javafx.geometry.Rectangle2D; @@ -65,16 +84,6 @@ import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; - -import static haveno.desktop.util.Layout.INITIAL_WINDOW_HEIGHT; -import static haveno.desktop.util.Layout.INITIAL_WINDOW_WIDTH; -import static haveno.desktop.util.Layout.MIN_WINDOW_HEIGHT; -import static haveno.desktop.util.Layout.MIN_WINDOW_WIDTH; - @Slf4j public class HavenoApp extends Application implements UncaughtExceptionHandler { @Setter @@ -327,30 +336,80 @@ public class HavenoApp extends Application implements UncaughtExceptionHandler { } private void shutDownByUser() { - boolean hasOpenOffers = false; + String potentialIssues = checkTradesAtShutdown() + checkDisputesAtShutdown() + checkOffersAtShutdown(); + promptUserAtShutdown(potentialIssues).thenAccept(asyncOkToShutDown -> { + if (asyncOkToShutDown) { + stop(); + } + }); + } + + private String checkTradesAtShutdown() { + log.info("Checking trades at shutdown"); + Instant fiveMinutesAgo = Instant.ofEpochSecond(Instant.now().getEpochSecond() - TimeUnit.MINUTES.toSeconds(5)); + for (Trade trade : injector.getInstance(TradeManager.class).getObservableList()) { + if (trade.getPhase().equals(Trade.Phase.DEPOSIT_REQUESTED) && + trade.getTakeOfferDate().toInstant().isAfter(fiveMinutesAgo)) { + String tradeDateString = DisplayUtils.formatDateTime(trade.getTakeOfferDate()); + String tradeInfo = Res.get("shared.tradeId") + ": " + trade.getShortId() + " " + + Res.get("shared.dateTime") + ": " + tradeDateString; + return Res.get("popup.info.shutDownWithTradeInit", tradeInfo) + System.lineSeparator() + System.lineSeparator(); + } + } + return ""; + } + + private String checkDisputesAtShutdown() { + log.info("Checking disputes at shutdown"); + if (injector.getInstance(ArbitrationManager.class).hasPendingMessageAtShutdown() || + injector.getInstance(MediationManager.class).hasPendingMessageAtShutdown() || + injector.getInstance(RefundManager.class).hasPendingMessageAtShutdown()) { + return Res.get("popup.info.shutDownWithDisputeInit") + System.lineSeparator() + System.lineSeparator(); + } + return ""; + } + + private String checkOffersAtShutdown() { + log.info("Checking offers at shutdown"); for (OpenOffer openOffer : injector.getInstance(OpenOfferManager.class).getObservableList()) { if (openOffer.getState().equals(OpenOffer.State.AVAILABLE)) { - hasOpenOffers = true; - break; + return Res.get("popup.info.shutDownWithOpenOffers") + System.lineSeparator() + System.lineSeparator(); } } - if (!hasOpenOffers) { - // No open offers, so no need to show the popup. - stop(); - return; - } + return ""; + } - // We show a popup to inform user that open offers will be removed if Haveno is not running. - String key = "showOpenOfferWarnPopupAtShutDown"; + private CompletableFuture promptUserAtShutdown(String issueInfo) { + final CompletableFuture asyncStatus = new CompletableFuture<>(); + if (issueInfo.length() > 0) { + // We maybe show a popup to inform user that some issues are pending + String key = Utilities.encodeToHex(Hash.getSha256Hash(issueInfo)); + if (injector.getInstance(Preferences.class).showAgain(key) && !DevEnv.isDevMode()) { + new Popup().warning(issueInfo) + .actionButtonText(Res.get("shared.okWait")) + .onAction(() -> asyncStatus.complete(false)) + .closeButtonText(Res.get("shared.closeAnywayDanger")) + .onClose(() -> asyncStatus.complete(true)) + .dontShowAgainId(key) + .width(800) + .show(); + return asyncStatus; + } + } + // if no warning popup has been shown yet, prompt user if they really intend to shut down + String key = "popup.info.shutDownQuery"; if (injector.getInstance(Preferences.class).showAgain(key) && !DevEnv.isDevMode()) { - new Popup().information(Res.get("popup.info.shutDownWithOpenOffers")) + new Popup().headLine(Res.get("popup.info.shutDownQuery")) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> asyncStatus.complete(true)) + .closeButtonText(Res.get("shared.no")) + .onClose(() -> asyncStatus.complete(false)) .dontShowAgainId(key) - .useShutDownButton() - .closeButtonText(Res.get("shared.cancel")) .show(); } else { - stop(); + asyncStatus.complete(true); } + return asyncStatus; } // Used for debugging trade process diff --git a/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java b/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java index 51093841b5..7f1f2574cf 100644 --- a/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java +++ b/desktop/src/main/java/haveno/desktop/main/overlays/windows/ContractWindow.java @@ -125,8 +125,6 @@ public class ContractWindow extends Overlay { boolean showAcceptedCountryCodes = acceptedCountryCodes != null && !acceptedCountryCodes.isEmpty(); int rows = 18; - if (dispute.getDepositTxSerialized() != null) - rows++; if (dispute.getPayoutTxSerialized() != null) rows++; if (dispute.getDelayedPayoutTxId() != null) diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index b82ea8a649..ea2d60267d 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -473,9 +473,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { // trade.getMaxTradePeriodDate().getTime(), // trade.getContract(), // trade.getContractHash(), -// depositTxSerialized, // payoutTxSerialized, -// depositTxHashAsString, // payoutTxHashAsString, // trade.getContractAsJson(), // trade.getMakerContractSignature(), @@ -503,9 +501,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { trade.getMaxTradePeriodDate().getTime(), trade.getContract(), trade.getContractHash(), - depositTxSerialized, payoutTxSerialized, - depositTxId, payoutTxHashAsString, trade.getContractAsJson(), trade.getMaker().getContractSignature(), diff --git a/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java index 3101f6122e..698bff0ac2 100644 --- a/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/haveno/desktop/main/support/dispute/DisputeView.java @@ -440,9 +440,6 @@ public abstract class DisputeView extends ActivatableView { return FilterResult.SELLER_ACCOUNT_DETAILS; } - if (dispute.getDepositTxId() != null && dispute.getDepositTxId().contains(filter)) { - return FilterResult.DEPOSIT_TX; - } if (dispute.getPayoutTxId() != null && dispute.getPayoutTxId().contains(filter)) { return FilterResult.PAYOUT_TX; } diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 9c00dbd359..c7994aad73 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -691,28 +691,26 @@ message Dispute { int64 trade_date = 9; Contract contract = 10; bytes contract_hash = 11; - bytes deposit_tx_serialized = 12; - bytes payout_tx_serialized = 13; - string deposit_tx_id = 14; - string payout_tx_id = 15; - string contract_as_json = 16; - bytes maker_contract_signature = 17; - bytes taker_contract_signature = 18; - PaymentAccountPayload maker_payment_account_payload = 19; - PaymentAccountPayload taker_payment_account_payload = 20; - PubKeyRing agent_pub_key_ring = 21; - bool is_support_ticket = 22; - repeated ChatMessage chat_message = 23; - bool is_closed = 24; - DisputeResult dispute_result = 25; - string dispute_payout_tx_id = 26; - SupportType support_type = 27; - string mediators_dispute_result = 28; - string delayed_payout_tx_id = 29; - string donation_address_of_delayed_payout_tx = 30; - State state = 31; - int64 trade_period_end = 32; - map extra_data = 33; + bytes payout_tx_serialized = 12; + string payout_tx_id = 13; + string contract_as_json = 14; + bytes maker_contract_signature = 15; + bytes taker_contract_signature = 16; + PaymentAccountPayload maker_payment_account_payload = 17; + PaymentAccountPayload taker_payment_account_payload = 18; + PubKeyRing agent_pub_key_ring = 19; + bool is_support_ticket = 20; + repeated ChatMessage chat_message = 21; + bool is_closed = 22; + DisputeResult dispute_result = 23; + string dispute_payout_tx_id = 24; + SupportType support_type = 25; + string mediators_dispute_result = 26; + string delayed_payout_tx_id = 27; + string donation_address_of_delayed_payout_tx = 28; + State state = 29; + int64 trade_period_end = 30; + map extra_data = 31; } message Attachment {