diff --git a/core/src/main/java/bisq/core/app/DomainInitialisation.java b/core/src/main/java/bisq/core/app/DomainInitialisation.java index 3f537d5a6f..d68f24205c 100644 --- a/core/src/main/java/bisq/core/app/DomainInitialisation.java +++ b/core/src/main/java/bisq/core/app/DomainInitialisation.java @@ -176,6 +176,10 @@ public class DomainInitialisation { PersistenceManager.onAllServicesInitialized(); + arbitratorManager.onAllServicesInitialized(); + mediatorManager.onAllServicesInitialized(); + refundAgentManager.onAllServicesInitialized(); + tradeManager.onAllServicesInitialized(); arbitrationManager.onAllServicesInitialized(); mediationManager.onAllServicesInitialized(); @@ -192,10 +196,6 @@ public class DomainInitialisation { walletAppSetup.setRejectedTxErrorMessageHandler(rejectedTxErrorMessageHandler, openOfferManager, tradeManager); - arbitratorManager.onAllServicesInitialized(); - mediatorManager.onAllServicesInitialized(); - refundAgentManager.onAllServicesInitialized(); - privateNotificationManager.privateNotificationProperty().addListener((observable, oldValue, newValue) -> { if (displayPrivateNotificationHandler != null) displayPrivateNotificationHandler.accept(newValue); diff --git a/core/src/main/java/bisq/core/support/SupportManager.java b/core/src/main/java/bisq/core/support/SupportManager.java index 42e98829fa..b564aa35f4 100644 --- a/core/src/main/java/bisq/core/support/SupportManager.java +++ b/core/src/main/java/bisq/core/support/SupportManager.java @@ -22,13 +22,15 @@ import bisq.core.api.CoreNotificationService; import bisq.core.locale.Res; import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.SupportMessage; - +import bisq.core.trade.protocol.TradeProtocol; +import bisq.core.trade.protocol.TradeProtocol.MailboxMessageComparator; import bisq.network.p2p.AckMessage; import bisq.network.p2p.AckMessageSourceType; import bisq.network.p2p.DecryptedMessageWithPubKey; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; import bisq.network.p2p.SendMailboxMessageListener; +import bisq.network.p2p.mailbox.MailboxMessage; import bisq.network.p2p.mailbox.MailboxMessageService; import bisq.common.Timer; @@ -36,6 +38,7 @@ import bisq.common.UserThread; import bisq.common.crypto.PubKeyRing; import bisq.common.proto.network.NetworkEnvelope; +import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -322,9 +325,27 @@ public abstract class SupportManager { private void applyMessages() { synchronized (lock) { - decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey)); + + // apply non-mailbox messages + decryptedDirectMessageWithPubKeys.stream() + .filter(e -> !(e.getNetworkEnvelope() instanceof MailboxMessage)) + .forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey)); + decryptedMailboxMessageWithPubKeys.stream() + .filter(e -> !(e.getNetworkEnvelope() instanceof MailboxMessage)) + .forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey)); + + // apply mailbox messages in order + decryptedDirectMessageWithPubKeys.stream() + .filter(e -> (e.getNetworkEnvelope() instanceof MailboxMessage)) + .sorted(new DecryptedMessageWithPubKeyComparator()) + .forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey)); + decryptedMailboxMessageWithPubKeys.stream() + .filter(e -> (e.getNetworkEnvelope() instanceof MailboxMessage)) + .sorted(new DecryptedMessageWithPubKeyComparator()) + .forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey)); + + // clear messages decryptedDirectMessageWithPubKeys.clear(); - decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey)); decryptedMailboxMessageWithPubKeys.clear(); } } @@ -351,4 +372,22 @@ public abstract class SupportManager { mailboxMessageService.removeMailboxMsg(ackMessage); } } + + private static class DecryptedMessageWithPubKeyComparator implements Comparator { + + MailboxMessageComparator mailboxMessageComparator; + public DecryptedMessageWithPubKeyComparator() { + mailboxMessageComparator = new TradeProtocol.MailboxMessageComparator(); + } + + @Override + public int compare(DecryptedMessageWithPubKey m1, DecryptedMessageWithPubKey m2) { + if (m1.getNetworkEnvelope() instanceof MailboxMessage) { + if (m2.getNetworkEnvelope() instanceof MailboxMessage) return mailboxMessageComparator.compare((MailboxMessage) m1.getNetworkEnvelope(), (MailboxMessage) m2.getNetworkEnvelope()); + else return 1; + } else { + return m2.getNetworkEnvelope() instanceof MailboxMessage ? -1 : 0; + } + } + } } diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index c5072efc52..8db2ba10de 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -97,7 +97,7 @@ public abstract class DisputeManager> extends Sup protected final TradeManager tradeManager; protected final ClosedTradableManager closedTradableManager; protected final OpenOfferManager openOfferManager; - protected final PubKeyRing pubKeyRing; + protected final KeyRing keyRing; protected final DisputeListService disputeListService; private final Config config; private final PriceFeedService priceFeedService; @@ -105,9 +105,6 @@ public abstract class DisputeManager> extends Sup @Getter protected final ObservableList validationExceptions = FXCollections.observableArrayList(); - @Getter - private final KeyPair signatureKeyPair; - /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -132,18 +129,20 @@ public abstract class DisputeManager> extends Sup this.tradeManager = tradeManager; this.closedTradableManager = closedTradableManager; this.openOfferManager = openOfferManager; - this.pubKeyRing = keyRing.getPubKeyRing(); - signatureKeyPair = keyRing.getSignatureKeyPair(); + this.keyRing = keyRing; this.disputeListService = disputeListService; this.config = config; this.priceFeedService = priceFeedService; } - /////////////////////////////////////////////////////////////////////////////////////////// // Implement template methods /////////////////////////////////////////////////////////////////////////////////////////// + public KeyPair getSignatureKeyPair() { + return keyRing.getSignatureKeyPair(); + } + @Override public void requestPersistence() { disputeListService.requestPersistence(); @@ -204,7 +203,7 @@ public abstract class DisputeManager> extends Sup // Abstract methods /////////////////////////////////////////////////////////////////////////////////////////// - // We get that message at both peers. The dispute object is in context of the trader + // We get this message at both peers. The dispute object is in context of the trader public abstract void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage); public abstract NodeAddress getAgentNodeAddress(Dispute dispute); @@ -292,7 +291,7 @@ public abstract class DisputeManager> extends Sup } public boolean isTrader(Dispute dispute) { - return pubKeyRing.equals(dispute.getTraderPubKeyRing()); + return keyRing.getPubKeyRing().equals(dispute.getTraderPubKeyRing()); } public Optional findOwnDispute(String tradeId) { @@ -352,7 +351,7 @@ public abstract class DisputeManager> extends Sup ChatMessage chatMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), - pubKeyRing.hashCode(), + keyRing.getPubKeyRing().hashCode(), false, Res.get("support.systemMsg", sysMsg), p2PService.getAddress()); @@ -389,7 +388,7 @@ public abstract class DisputeManager> extends Sup // 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.setDisputeState(Trade.DisputeState.DISPUTE_OPENED); + trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED); requestPersistence(); resultHandler.handleResult(); } @@ -405,7 +404,7 @@ public abstract class DisputeManager> extends Sup // 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.setDisputeState(Trade.DisputeState.DISPUTE_OPENED); + trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED); requestPersistence(); resultHandler.handleResult(); } @@ -507,7 +506,7 @@ public abstract class DisputeManager> extends Sup Optional storedDisputeOptional = findDispute(dispute); if (!storedDisputeOptional.isPresent()) { disputeList.add(dispute); - trade.setDisputeState(Trade.DisputeState.DISPUTE_OPENED); + trade.setDisputeStateIfProgress(Trade.DisputeState.DISPUTE_OPENED); // send dispute opened message to peer if arbitrator if (trade.isArbitrator()) sendDisputeOpenedMessageToPeer(dispute, contract, dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(), trade.getSelf().getUpdatedMultisigHex()); @@ -703,22 +702,25 @@ public abstract class DisputeManager> extends Sup // create dispute payout tx if not given if (payoutTx == null) payoutTx = createDisputePayoutTx(trade, dispute, disputeResult, false); // can be null if already published or we don't have receiver's multisig hex - // persist result in dispute's chat message - ChatMessage chatMessage = new ChatMessage( - getSupportType(), - dispute.getTradeId(), - dispute.getTraderPubKeyRing().hashCode(), - false, - summaryText, - p2PService.getAddress()); - disputeResult.setChatMessage(chatMessage); - dispute.addAndPersistChatMessage(chatMessage); + // persist result in dispute's chat message once + boolean resending = disputeResult.getChatMessage() != null; + if (!resending) { + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + dispute.getTraderPubKeyRing().hashCode(), + false, + summaryText, + p2PService.getAddress()); + disputeResult.setChatMessage(chatMessage); + dispute.addAndPersistChatMessage(chatMessage); + } // create dispute closed message TradingPeer receiver = trade.getTradingPeer(dispute.getTraderPubKeyRing()); String unsignedPayoutTxHex = payoutTx == null ? null : payoutTx.getTxSet().getMultisigTxHex(); TradingPeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer(); - boolean deferPublishPayout = unsignedPayoutTxHex != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal() ; + boolean deferPublishPayout = !resending && unsignedPayoutTxHex != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal() ; DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult, p2PService.getAddress(), UUID.randomUUID().toString(), @@ -731,7 +733,7 @@ public abstract class DisputeManager> extends Sup log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}", disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), disputeClosedMessage.getClass().getSimpleName(), disputeClosedMessage.getTradeId(), - disputeClosedMessage.getUid(), chatMessage.getUid()); + disputeClosedMessage.getUid(), disputeResult.getChatMessage().getUid()); mailboxMessageService.sendEncryptedMailboxMessage(receiver.getNodeAddress(), dispute.getTraderPubKeyRing(), disputeClosedMessage, @@ -742,11 +744,11 @@ public abstract class DisputeManager> extends Sup "chatMessage.uid={}", disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), - chatMessage.getUid()); + disputeResult.getChatMessage().getUid()); // We use the chatMessage wrapped inside the DisputeClosedMessage for // the state, as that is displayed to the user and we only persist that msg - chatMessage.setArrived(true); + disputeResult.getChatMessage().setArrived(true); trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG); trade.syncWalletNormallyForMs(30000); requestPersistence(); @@ -759,11 +761,11 @@ public abstract class DisputeManager> extends Sup "chatMessage.uid={}", disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), - chatMessage.getUid()); + disputeResult.getChatMessage().getUid()); // We use the chatMessage wrapped inside the DisputeClosedMessage for // the state, as that is displayed to the user and we only persist that msg - chatMessage.setStoredInMailbox(true); + disputeResult.getChatMessage().setStoredInMailbox(true); Trade trade = tradeManager.getTrade(dispute.getTradeId()); trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG); requestPersistence(); @@ -776,11 +778,11 @@ public abstract class DisputeManager> extends Sup "chatMessage.uid={}, errorMessage={}", disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), - chatMessage.getUid(), errorMessage); + disputeResult.getChatMessage().getUid(), errorMessage); // We use the chatMessage wrapped inside the DisputeClosedMessage for // the state, as that is displayed to the user and we only persist that msg - chatMessage.setSendMessageError(errorMessage); + disputeResult.getChatMessage().setSendMessageError(errorMessage); trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG); requestPersistence(); faultHandler.handleFault(errorMessage, new RuntimeException(errorMessage)); @@ -914,7 +916,7 @@ public abstract class DisputeManager> extends Sup } private boolean isAgent(Dispute dispute) { - return pubKeyRing.equals(dispute.getAgentPubKeyRing()); + return keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing()); } private Optional findDispute(Dispute dispute) { @@ -987,7 +989,7 @@ public abstract class DisputeManager> extends Sup ChatMessage mediatorsDisputeClosedMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), - pubKeyRing.hashCode(), + keyRing.getPubKeyRing().hashCode(), false, mediatorsDisputeResult, p2PService.getAddress()); @@ -1074,7 +1076,7 @@ public abstract class DisputeManager> extends Sup ChatMessage priceInfoMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), - pubKeyRing.hashCode(), + keyRing.getPubKeyRing().hashCode(), false, priceInfoText, p2PService.getAddress()); diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeSummaryVerification.java b/core/src/main/java/bisq/core/support/dispute/DisputeSummaryVerification.java index 8e81de180e..f3b6c98168 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeSummaryVerification.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeSummaryVerification.java @@ -88,6 +88,7 @@ public class DisputeSummaryVerification { throw new IllegalArgumentException(Res.get("support.sigCheck.popup.failed")); } } catch (Throwable e) { + e.printStackTrace(); throw new IllegalArgumentException(Res.get("support.sigCheck.popup.invalidFormat")); } } diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java index 384a0cfcf9..7cf25d62d0 100644 --- a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java @@ -211,7 +211,7 @@ public final class ArbitrationManager extends DisputeManager { + public static class MailboxMessageComparator implements Comparator { private static List> messageOrder = Arrays.asList( + AckMessage.class, DepositsConfirmedMessage.class, PaymentSentMessage.class, PaymentReceivedMessage.class, @@ -397,7 +401,8 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D .from(sender)) .setup(tasks( ProcessDepositsConfirmedMessage.class, - VerifyPeersAccountAgeWitness.class) + VerifyPeersAccountAgeWitness.class, + ResendDisputeClosedMessageWithPayout.class) .using(new TradeTaskRunner(trade, () -> { handleTaskRunnerSuccess(sender, response); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ResendDisputeClosedMessageWithPayout.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ResendDisputeClosedMessageWithPayout.java new file mode 100644 index 0000000000..8ebd32a193 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ResendDisputeClosedMessageWithPayout.java @@ -0,0 +1,84 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package bisq.core.trade.protocol.tasks; + + +import bisq.common.taskrunner.TaskRunner; +import bisq.core.support.dispute.Dispute; +import bisq.core.trade.HavenoUtils; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.DepositsConfirmedMessage; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.util.Validator; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.List; + +@Slf4j +public class ResendDisputeClosedMessageWithPayout extends TradeTask { + + @SuppressWarnings({"unused"}) + public ResendDisputeClosedMessageWithPayout(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + // get peer + DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage(); + checkNotNull(request); + Validator.checkTradeId(processModel.getOfferId(), request); + TradingPeer sender = trade.getTradingPeer(request.getPubKeyRing()); + if (sender == null) throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller"); + + // arbitrator resends DisputeClosedMessage with payout tx when updated multisig info received + boolean ticketClosed = false; + if (!trade.isPayoutPublished() && trade.isArbitrator()) { + List disputes = trade.getDisputes(); + for (Dispute dispute : disputes) { + if (!dispute.isClosed()) continue; // dispute must be closed + if (sender.getPubKeyRing().equals(dispute.getTraderPubKeyRing())) { + HavenoUtils.arbitrationManager.closeDisputeTicket(dispute.getDisputeResultProperty().get(), dispute, dispute.getDisputeResultProperty().get().summaryNotesProperty().get(), null, () -> { + completeAux(); + }, (errMessage, err) -> { + err.printStackTrace(); + failed(err); + }); + ticketClosed = true; + break; + } + } + } + + // complete if not waiting for result + if (!ticketClosed) completeAux(); + } catch (Throwable t) { + failed(t); + } + } + + private void completeAux() { + processModel.getTradeManager().requestPersistence(); + complete(); + } +}