From d55114e019fda469266dab50f63916289b2e71cb Mon Sep 17 00:00:00 2001 From: chimp1984 <54558767+chimp1984@users.noreply.github.com> Date: Mon, 9 Sep 2019 20:51:57 +0200 Subject: [PATCH] Add mediation support (#3221) * Refactoring: Move arbitration package inside dispute package * Use abstract base class DisputeResolver for arbitrator * Refactoring: Move mediator to mediator package. * Let Mediator inherit DisputeResolver. * Do not use protobuf inheritance - Do not use protobuf inheritance for Arbitrator and Mediator as it would break backward compatibility (and protobuf inheritance sucks anyway) * Refactoring: Move ArbitratorModule to parent package * Refactoring: Rename ArbitratorModule to DisputeModule * Add mediators to Filter * Add mediators to filter window * Use abstract DisputeResolverService as base class for ArbitratorService - Add common base class for ArbitratorService and MediatorService * Fix test * Use abstract DisputeResolverManager as base class for ArbitratorManager - Add common base class for ArbitratorManager and MediatorManager * Refactor: Move arbitratorregistration package inside register pkg * Refactor: Rename arbitratorregistration package to arbitrator * Add registration view for mediator - With cmd+D one can open the mediator regisration in account screen. For arbitrator its cmd+R * Separate pub key list for mediator (no new keys added yet) * Set new pubkeys for mediator registration - Before release set new keys from maintainer who manages keys * Set disputes @Nullable. Add null checks * Remove pre v0.9 handling for supported arbitrators from offer - We changed handling of arbitrator selection with v0.9 so the supported arbitrators in the offer is not used anymore. As we enforced v1.2 a while back for trading we can be sure no pre v0.9 clients are used anymore and we can remove the optional code part. * Remove supported arbitrators info in offer details window - As we do not use supported arbitraors in offer anymore since v0.9 we can remove that. * Remove check for matching arbitrator languages As we do not use the supported arbitratos from offer since v0.9 we can remove that check. * Remove not used classes * Remove checks for arbitrator and mediator in offer We do not use those fields anymore. We still need to keep the fields not nullable as old clients have the check still. * Add check if sig of proto object is not empty We got in dev testing sometimes an empty protobuf Alert. Might be caused from protobuf copatibility issues during development but not 100% clear. As it causes an exception and corrupted user db file we prefer to set it to null. * Remove TakerSelectMediator This is not used anymore. Currently we would get an exception in the trade but with follow up changes we will fix that... Mediator handling and selection will be done the same way like arbitrator. The current mediator handling was a relict from earlier partial support for mediators which never got completed. As still a null check is in place we need to ensure backward compatibility. * Set arbitratorNodeAddresses and mediatorNodeAddresses to deprecated We do not use arbitratorNodeAddresses and mediatorNodeAddresses anymore but as there is a null check we still need to keep the field ans set it to an empty arrayList. * Make ArbitratorSelection generic. Add MEDIATOR_ADDRESS We want to use the same selection algorithm for mediators as for arbitrators, so we make ArbitratorSelection generic. We add MEDIATOR_ADDRESS as extraMap entry to TradeStatistics2 to be able to track number of trades with specific mediators. ExtraMap is used to add new data to existing protobuf definitions which is supported also by not updated clients. Adding a new protobuf field would only be supported by new clients. As mediator support is a new feature we could add a new field but to keep it in the same style like arbitrator we prefer to use the map here as well. * Refactor: Rename ArbitratorSelection to DisputeResolverSelection * Add mediator to OfferAvailabilityResponse and mediatorNodeAddress to OpenOffer WIP for supporting mediator selection the same way like arbitrators. * Make arbitrator not nullable We can ensure that all users are post v0.9 so we can remove the nullable support. * Add selectedMediator to OfferAvailabilityModel Remove nullable support in ProcessOfferAvailabilityResponse as we can ensure all clients are post v0.9 * Refactor: Rename method * Add todo for using more generic keys for display strings * Refactor: Rename method * Fix wrong handling of registeredMediator Fix copy/paste error * Add mediatorNodeAddress to trade * Handle nullable mediator in ProcessOfferAvailabilityResponse We do not get the mediator set from old clients but we expect a not null value so we use the DisputeResolverSelection in case it is null. We need to pass mediatorManager and tradeStatisticsManager to the OfferAvailabilityModel. * Change log level, cleanup * Revert changes in OfferPayload due backward compatibility issues Because of backward compatibility issues we needed to revert the removal of arbitratorNodeAddresses and mediatorNodeAddresses. The signature check for the offer would fail as an old client would send a not-empty list but new clients would have had an empty list, so the hash would be different and the sig check fail and we would not accept that offer. That is the reason why we still need to support those data even it is not used anymore. This is one of the more tricky cases for backward compatibility issues. This version now is tested between new and old clients and trade and disputes work. * Add checks if any mediator is available * Cleanup classes * Fix test * Add mediator DisputeStates Add isMediationDispute to Dispute class. If a dispute opening gets requested we check if state is DisputeState.NO_DISPUTE and the open mediation. If state is DisputeState.MEDIATION_REQUESTED we open arbitration. * Cleanup; support isMediationDispute * Handle mediator data in Dispute domain - Add getConflictResolverNodeAddress method to Dispute to resolve arbitrator or mediator address based on isMediationDispute flag. - Rename arbitratorPubKeyRing to conflictResolverPubKeyRing in Dispute. We cannot rename arbitratorPubKeyRing in the protobuf definition as it would break backward compatibility. * Add support for mediation in dispute domain - Add isMediationDispute method to ChatSession - Add isMediationDispute method to DisputeCommunicationMessage - Add isMediationDispute to dispute id - Refactor findDispute method - Add null checks - Cleanups * Remove impossible case Reserved and locked funds are used for offers and trades only. * Fix typos * Handle mediator and arbitrator strings - Work in progress of adjusting correct terms. - Cleanups * Refactor: Rename arbitrator package to disputeresolvers * Refactor: Rename ArbitratorDisputeView classes to DisputeResolverView * Add support for close ticket from mediator (WIP) In mediator case we do not create any transaction but only send the dispute result which contains the mediators recommended payout distribution. At teh traders we set the disputeState in the trade to closed. This will be used in the next commits to update the trade so that the traders get displayed the recommended payout and get asked if they agree to that. * Refactoring: Rename class Rename MessageDeliveryFailedException to DisputeMessageDeliveryFailedException * Refactoring: Move dispute classes to dispute package * Refactoring: Move Attachment class to dispute package * Refactoring: Move package one level up Move bisq.core.dispute.arbitration.messages to bisq.core.dispute.messages * Add todo comment * Use ARBITRATION instead of DISPUTE * Make DisputeManager abstract base class for ArbitrationDisputeManager WIP for separating DisputeManager to ArbitrationDisputeManager and MediationDisputeManager * Add MediationDisputeManager * Add MediationDisputeManager and ArbitrationDisputeManager to test * Add mediationDisputeManager to relevant classes There are some cases where arbitrationDisputeManager only is used. Those are usually related to the payout tx. As mediators do not do a payout we don't need it there. * Add TradersArbitrationDisputeView and TradersMediationDisputeView WIP for separating TraderDisputeView * Refactor: Rename class * Refactor: Rename support.tab.support to support.tab.mediation.support I am aware that committing non default translation files is not recommended, but I think in that case it helps to avoid to show errors for developers who use non-english locale. The changes will be overwritten by transifex once it gets synced... * Add DisputeView as common base class Further refactor separation of diff. dispute views * Refactor: Rename package * Refactor: Rename DisputesView to SupportView * Refactor: Rename package * Add MediationDisputeManager to CorePersistedDataHost * Add MediationDisputeList as db file, refactor DisputeList WIP for making Dispute domain more generic. We want to separate arbitration and mediation clearly. * Further refactoring to split mediation and arbitration * Further refactoring to split mediation and arbitration Move methods used for arbitration only to ArbitrationDisputeManager * Refactor: Rename package Rename bisq.core.dispute to bisq.core.support No other changes in that commit. We want to improve the data structure with the trader chat. Support will be the top level. Then dispute containing arbitration and mediation. Next to dispute will be trader chat. bisq.core.support bisq.core.support.dispute.arbitration bisq.core.support.dispute.mediation bisq.core.support.traderchat (not happy with name for that yet) * Refactor: Move dispute domain classes into isq.core.support.dispute package * Refactor: Move classes Move bisq.core.chat.ChatSession to bisq.core.support.ChatSession Move bisq.core.chat.ChatManager to bisq.core.support.ChatManager Move bisq.core.trade.TradeChatSession to bisq.core.support.traderchat.TradeChatSession * Refactor: Move DisputeCommunicationMessage * Refactor: Rename DisputeCommunicationMessage to ChatMessage * Add comments * Refactor: Move class * Refactor: Rename class * Refactor: Rename addDisputeCommunicationMessage and strings and variables Rename disputeCommunicationMessage to chatMessage * Refactor: Rename method * Refactor: Rename methods and strings * Add ArbitrationChatMessage and DisputeChatMessage * Refactor: Rename class * Move ChatMessage.Type to SupportType Add to all supportMessages the SupportType so that we can filter in our chatSessions the messages we are interested in. * Refactor: Move classed to new package * Refactor: Rename package * Refactor: Move classed to new package * Refactor: Move classed to new package * Refactor: Rename classes * Refactor: Rename package * Refactor: Rename classes * Refactor: Rename classes * Remove empty DisputeModule * Refactor: Rename classes * Refactor SupportManager domain (WIP) * Refactor SupportSession domain (WIP) * Remove methods from SupportSession * Dont expose p2pService in SupportManager * Remove supportType in SupportSession * Remove supportSession from getPeerNodeAddress method * Remove isBuyer from supportSession * Move creation of ChatMessage to SupportManager * Remove isMediationDispute fielf in ChatMessage * Remove chatMessage.isMediationDispute() * Refactor: Rename trade.getCommunicationMessages() * Move creation of ChatMessage to Chat * Refactor: Rename class * Refactor: Move ChatView class * Refactor: Move PriceFeedComboBoxItem class to shared package * Refactor: Use 'public abstract' instead of 'abstract public' * Refactor: Use 'protected abstract' instead of 'abstract protected' * Add traderChatManager.onAllServicesInitialized() to BisqSetup * Remove unused param * Refactor: Rename addChatMessage to addAndPersistChatMessage * Fix missing check at ack msg handling Various WIP refactorings/improvements * Remove addAndPersistChatMessage from SupportSession * Remove disputeManager from DisputeSession * Fix missing getConcreteDisputeChatSession impl. * Refactor: Rename package * Refactor: Rename classes Avoid trader as it might confuse with trader chat. As for mediation/arbitration the agent (mediator/arbitrator) are acting a bit like a server we use the client terminology for the traders. * Refactor: Move classes to new package * Fix missing protobuf data - Add missing SupportType to protobuf - Remove is_mediation_dispute from Dispute protobuf definition - Add getAgentNodeAddress method - Var. other refactorings, cleanups * Clone list at persisting to avoid ConcurrentModificationException * Fix order of SupportType Old clients fall back to enum at slot 0. * Add getDisputeState_StartedByPeer template method * Add trade protocol tasks for mediation result tx signing and msg sending * Complete protocol tasks for mediation * Refactor: Remove unneeded SuppressWarnings type: "WeakerAccess" * Complete mediation result protocol Works now all but not much tested.... * Add activation date and capability We need to make sure that not updated users cannot cause problems once mediation is supported. We would get mixed cases where one has a mediation ticket and the not updated user an arbitration ticket. To avoid that we set an activation date with about 10 days from release. Until that date mediation is not supported. Additionally we use OfferRestrictions.REQUIRE_UPDATE_DATE for hiding offers from users how have not updated (we use the fact that mediator and arbitrator has been same in old version, in new version they are different). An old client cannot take an offer from a new maker as he does not has set the new MEDIATION capability. He will get an null value as AvailabilityResult as he has not the new entry MISSING_MANDATORY_CAPABILITY. We will also use the min version for trading in the filter, so that not updated users get a popup telling them to update and they see all offers deactivated. * Various fixes * Remove code part which does not make sense (anymore) Maybe in older versions there was use of openDisputes and closedDisputes but now it does not make sense anymore and arbitrator never gets 4 cases opened if offline. * Add check of balance is > 0 * Only close trade if payout tx is set * Add missing check if arbitrator and mediator are available * Fix wrong key * Improve handling of checks and popup display For create and take offer we check certain conditions and show a popup if not met. This commit moves that to GuiUtils. * Rename any occurrance of DisputeResolver to DisputeAgent * Fix handling of mediatorPubKeyRing * Remove disputeSummaryWindow.evidence fields * Add missing persistence for MediationResultState * Fix tests * Make text more compact to not exceed space * Refactor NotificationGroup * Improve text, add dev testing feature for popups * Improve text * Renamed a key and assigned a new text * Fix states * Do not set errorMessage Do not set errorMessage if both peers have opened a dispute and agent was not online * Remove logs used for dev testing * Fix getMedian method with empty list * Add new methods and tests Add fromCommaSeparatedOrdinals and toCommaSeparatedOrdinals to convert from string representations (used for handling backward compatibility with mediation release). Add check if int >= 0 to fromIntList * Move error log outside of delayed call * Add capabilities entry to extraDataMap in offer The previous implementation did not work for supporting updates and hiding offers from not updated clients. We use now the capabilities converted to a string list and put it into the extraDataMap. If a use with old persisted offers updates his offers gets converted to add the capabilities. Updated clients will ignore offers without the mediation capability set in the offer. * Rename non sync protobuf definitions As Christoph Sturm pointed out we can rename protobuf entries. Only index number must not be changed. * Fix UI state when arbitration has started Only set mediation state if we are not in arbitration state. * Remove restriction * Fix typo; remove errorMessage If both have opened a dispute and agent was not online we dont treat it as error. * Improve text * Store full address for localhost dev testing The arbitrator/mediator selection is based on statistics of usage of agents in past trades. We put the first 4 chars into the trade statistics, but for localhost that would be same vale for 2 diff nodes. * Remove errorMessage If both have opened a dispute and agent was not online we dont treat it as error. * Improve text * Keep accept or reject button enabled after accept - If peer never accepts the trader who has accepted first can change to reject to open a arbitration dispute. We could improve that by adding a new state to open arbitration directly and show a diff. button text and popup. But I think for now thats ok as well.... * Cleanups (no functional change) - remove unused params - remove not used code - reformat - clean up comments - fix log levels - remove redundant annotations * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain * Improve text * Auto fill remaining amount in custom payout If mediator or arbitrator are doing a custom payout, we auto-fill counterpart field with remaining amount, so he does not need to calculate. --- .idea/codeStyles/codeStyleConfig.xml | 1 + .../java/bisq/common/app/Capabilities.java | 50 +- .../main/java/bisq/common/app/Capability.java | 6 +- .../src/main/java/bisq/common/app/DevEnv.java | 1 - .../java/bisq/common/taskrunner/Task.java | 2 +- .../main/java/bisq/common/util/MathUtils.java | 6 +- common/src/main/proto/pb.proto | 96 +- .../bisq/common/app/CapabilitiesTest.java | 50 +- core/src/main/java/bisq/core/CoreModule.java | 6 - .../account/sign/SignedWitnessService.java | 18 +- core/src/main/java/bisq/core/alert/Alert.java | 8 +- .../java/bisq/core/app/BisqExecutable.java | 4 +- .../main/java/bisq/core/app/BisqSetup.java | 42 +- .../app/misc/ExecutableForAppWithP2p.java | 2 +- .../core/app/misc/ModuleForAppWithP2p.java | 6 - .../core/arbitration/ArbitratorManager.java | 375 ------- .../core/arbitration/ArbitratorService.java | 123 --- .../core/arbitration/DisputeChatSession.java | 177 ---- .../bisq/core/arbitration/DisputeManager.java | 929 ------------------ .../core/btc/wallet/TradeWalletService.java | 121 ++- .../main/java/bisq/core/chat/ChatManager.java | 219 ----- .../main/java/bisq/core/chat/ChatSession.java | 66 -- .../dao/governance/bond/BondRepository.java | 6 +- .../main/java/bisq/core/dao/node/BsqNode.java | 2 +- .../main/java/bisq/core/filter/Filter.java | 20 +- .../alerts/DisputeMsgEvents.java | 28 +- .../bisq/core/offer/AvailabilityResult.java | 3 +- core/src/main/java/bisq/core/offer/Offer.java | 11 +- .../bisq/core/offer/OfferBookService.java | 32 +- .../java/bisq/core/offer/OfferPayload.java | 18 +- .../bisq/core/offer/OfferRestrictions.java | 28 +- .../main/java/bisq/core/offer/OfferUtil.java | 26 +- .../main/java/bisq/core/offer/OpenOffer.java | 23 +- .../bisq/core/offer/OpenOfferManager.java | 212 +++- ...ection.java => DisputeAgentSelection.java} | 58 +- .../availability/OfferAvailabilityModel.java | 21 +- .../ProcessOfferAvailabilityResponse.java | 51 +- .../messages/OfferAvailabilityRequest.java | 2 + .../messages/OfferAvailabilityResponse.java | 25 +- .../offer/placeoffer/PlaceOfferModel.java | 2 +- .../placeoffer/tasks/CreateMakerFeeTx.java | 8 +- .../offer/placeoffer/tasks/ValidateOffer.java | 2 - .../CountryBasedPaymentAccountPayload.java | 4 +- .../payload/PaymentAccountPayload.java | 4 +- .../presentation/CorePresentationModule.java | 2 +- .../presentation/DisputePresentation.java | 49 - .../SupportTicketsPresentation.java | 78 ++ .../java/bisq/core/proto/ProtoDevUtil.java | 2 +- .../network/CoreNetworkProtoResolver.java | 24 +- .../CorePersistenceProtoResolver.java | 11 +- .../core/setup/CoreNetworkCapabilities.java | 5 +- .../core/setup/CorePersistedDataHost.java | 6 +- .../bisq/core/support/SupportManager.java | 320 ++++++ .../bisq/core/support/SupportSession.java | 56 ++ .../SupportType.java} | 24 +- .../dispute}/Attachment.java | 2 +- .../dispute}/Dispute.java | 55 +- .../dispute}/DisputeAlreadyOpenException.java | 2 +- .../dispute}/DisputeList.java | 54 +- .../support/dispute/DisputeListService.java | 190 ++++ .../core/support/dispute/DisputeManager.java | 701 +++++++++++++ ...isputeMessageDeliveryFailedException.java} | 6 +- .../dispute}/DisputeResult.java | 18 +- .../core/support/dispute/DisputeSession.java | 79 ++ .../support/dispute/agent/DisputeAgent.java | 113 +++ .../dispute/agent/DisputeAgentManager.java | 338 +++++++ .../dispute/agent/DisputeAgentService.java | 121 +++ .../arbitration/ArbitrationDisputeList.java | 83 ++ .../ArbitrationDisputeListService.java | 48 + .../arbitration/ArbitrationManager.java | 390 ++++++++ .../arbitration/ArbitrationSession.java | 33 + .../dispute}/arbitration/BuyerDataItem.java | 3 +- .../arbitration/arbitrator}/Arbitrator.java | 82 +- .../arbitrator/ArbitratorManager.java | 103 ++ .../arbitrator/ArbitratorService.java | 61 ++ .../messages/ArbitrationMessage.java | 27 + .../PeerPublishedDisputePayoutTxMessage.java | 27 +- .../mediation/MediationDisputeList.java | 82 ++ .../MediationDisputeListService.java | 48 + .../dispute/mediation/MediationManager.java | 251 +++++ .../mediation/MediationResultState.java | 46 + .../dispute/mediation/MediationSession.java | 33 + .../dispute/mediation/mediator}/Mediator.java | 68 +- .../mediation/mediator/MediatorManager.java | 101 ++ .../mediation/mediator/MediatorService.java | 66 ++ .../dispute/messages/DisputeMessage.java | 28 + .../messages/DisputeResultMessage.java | 23 +- .../messages/OpenNewDisputeMessage.java | 23 +- .../messages/PeerOpenedDisputeMessage.java | 23 +- .../messages/ChatMessage.java} | 149 +-- .../messages/SupportMessage.java} | 13 +- .../support/traderchat/TradeChatSession.java | 73 ++ .../support/traderchat/TraderChatManager.java | 187 ++++ .../bisq/core/trade/BuyerAsMakerTrade.java | 15 +- .../bisq/core/trade/BuyerAsTakerTrade.java | 15 +- .../main/java/bisq/core/trade/BuyerTrade.java | 24 +- .../main/java/bisq/core/trade/Contract.java | 25 + .../bisq/core/trade/SellerAsMakerTrade.java | 4 +- .../bisq/core/trade/SellerAsTakerTrade.java | 15 +- .../java/bisq/core/trade/SellerTrade.java | 24 +- core/src/main/java/bisq/core/trade/Trade.java | 134 +-- .../bisq/core/trade/TradeChatSession.java | 214 ---- .../java/bisq/core/trade/TradeManager.java | 30 +- .../MediatedPayoutTxPublishedMessage.java | 91 ++ .../MediatedPayoutTxSignatureMessage.java | 99 ++ .../trade/messages/PayDepositRequest.java | 4 +- .../protocol/ArbitratorSelectionRule.java | 51 - .../trade/protocol/BuyerAsMakerProtocol.java | 13 +- .../trade/protocol/BuyerAsTakerProtocol.java | 8 +- .../trade/protocol/MediatorSelectionRule.java | 53 - .../core/trade/protocol/ProcessModel.java | 23 +- .../trade/protocol/SellerAsMakerProtocol.java | 11 +- .../trade/protocol/SellerAsTakerProtocol.java | 6 +- .../core/trade/protocol/TradeProtocol.java | 125 ++- .../bisq/core/trade/protocol/TradingPeer.java | 6 + .../trade/protocol/tasks/ApplyFilter.java | 2 +- .../protocol/tasks/BroadcastPayoutTx.java | 85 ++ .../tasks/PublishTradeStatistics.java | 12 +- .../tasks/SendPayoutTxPublishedMessage.java | 98 ++ .../protocol/tasks/SetupPayoutTxListener.java | 124 +++ .../tasks/VerifyPeersAccountAgeWitness.java | 2 +- .../BuyerProcessPayoutTxPublishedMessage.java | 2 +- ...CounterCurrencyTransferStartedMessage.java | 2 +- .../buyer/BuyerSetupPayoutTxListener.java | 85 +- .../BuyerAsMakerCreatesAndSignsDepositTx.java | 4 +- .../BuyerAsMakerSignPayoutTx.java | 9 +- .../BuyerAsTakerCreatesDepositTxInputs.java | 2 +- .../BuyerAsTakerSignAndPublishDepositTx.java | 4 +- .../maker/MakerCreateAndSignContract.java | 2 +- ...MakerProcessDepositTxPublishedMessage.java | 2 +- .../maker/MakerProcessPayDepositRequest.java | 36 +- .../MakerSendPublishDepositTxRequest.java | 2 +- .../maker/MakerSetupDepositTxListener.java | 2 +- .../tasks/maker/MakerVerifyTakerAccount.java | 2 +- .../maker/MakerVerifyTakerFeePayment.java | 2 +- .../BroadcastMediatedPayoutTx.java} | 29 +- .../mediation/FinalizeMediatedPayoutTx.java | 121 +++ ...ProcessMediatedPayoutSignatureMessage.java | 61 ++ ...ocessMediatedPayoutTxPublishedMessage.java | 77 ++ .../SendMediatedPayoutSignatureMessage.java | 101 ++ .../SendMediatedPayoutTxPublishedMessage.java | 85 ++ .../SetupMediatedPayoutTxListener.java | 54 + .../tasks/mediation/SignMediatedPayoutTx.java | 110 +++ .../tasks/seller/SellerBroadcastPayoutTx.java | 53 +- ...CounterCurrencyTransferStartedMessage.java | 2 +- .../SellerSendPayoutTxPublishedMessage.java | 93 +- .../seller/SellerSignAndFinalizePayoutTx.java | 2 +- .../seller/SellerVerifiesPeersAccountAge.java | 2 +- ...SellerAsMakerCreatesAndSignsDepositTx.java | 4 +- .../SellerAsTakerCreatesDepositTxInputs.java | 2 +- .../SellerAsTakerSignAndPublishDepositTx.java | 4 +- .../tasks/taker/CreateTakerFeeTx.java | 8 +- .../TakerProcessPublishDepositTxRequest.java | 2 +- .../tasks/taker/TakerPublishFeeTx.java | 2 +- .../tasks/taker/TakerSelectMediator.java | 65 -- .../TakerSendDepositTxPublishedMessage.java | 2 +- .../taker/TakerSendPayDepositRequest.java | 3 +- .../taker/TakerVerifyAndSignContract.java | 2 +- .../tasks/taker/TakerVerifyMakerAccount.java | 2 +- .../taker/TakerVerifyMakerFeePayment.java | 2 +- .../trade/statistics/TradeStatistics2.java | 1 + core/src/main/java/bisq/core/user/User.java | 12 +- .../main/java/bisq/core/user/UserPayload.java | 6 +- .../resources/i18n/displayStrings.properties | 260 +++-- .../i18n/displayStrings_de.properties | 4 +- .../i18n/displayStrings_el.properties | 4 +- .../i18n/displayStrings_es.properties | 4 +- .../i18n/displayStrings_fa.properties | 4 +- .../i18n/displayStrings_fr.properties | 4 +- .../i18n/displayStrings_ja.properties | 4 +- .../i18n/displayStrings_pt.properties | 4 +- .../i18n/displayStrings_ru.properties | 4 +- .../i18n/displayStrings_th.properties | 4 +- .../i18n/displayStrings_vi.properties | 4 +- .../i18n/displayStrings_zh.properties | 4 +- .../sign/SignedWitnessServiceTest.java | 8 +- .../arbitration/ArbitratorManagerTest.java | 23 +- .../bisq/core/arbitration/ArbitratorTest.java | 2 + .../core/arbitration/BuyerDataItemTest.java | 1 + .../bisq/core/arbitration/MediatorTest.java | 2 + .../bisq/core/offer/OpenOfferManagerTest.java | 9 +- .../availability/ArbitratorSelectionTest.java | 18 +- .../core/user/UserPayloadModelVOTest.java | 20 +- .../GeneralAccountNumberForm.java | 2 +- .../paymentmethods/PaymentMethodForm.java | 6 +- .../main/java/bisq/desktop/main/MainView.java | 13 +- .../java/bisq/desktop/main/MainViewModel.java | 18 +- .../desktop/main/account/AccountView.java | 48 +- .../AgentRegistrationView.java} | 50 +- .../AgentRegistrationViewModel.java} | 112 +-- .../ArbitratorRegistrationView.fxml | 2 +- .../ArbitratorRegistrationView.java | 45 + .../ArbitratorRegistrationViewModel.java | 71 ++ .../mediator/MediatorRegistrationView.fxml | 27 + .../mediator/MediatorRegistrationView.java | 45 + .../MediatorRegistrationViewModel.java | 67 ++ .../main/dao/bonding/BondingViewUtils.java | 8 +- .../dao/governance/make/MakeProposalView.java | 8 +- .../main/dao/wallet/send/BsqSendView.java | 8 +- .../bisq/desktop/main/debug/DebugView.java | 2 - .../desktop/main/disputes/DisputesView.java | 166 ---- .../desktop/main/funds/locked/LockedView.java | 2 - .../main/funds/reserved/ReservedView.java | 2 - .../TransactionAwareTradableFactory.java | 10 +- .../transactions/TransactionAwareTrade.java | 12 +- .../transactions/TransactionsListItem.java | 2 +- .../funds/transactions/TransactionsView.java | 4 +- .../main/funds/withdrawal/WithdrawalView.java | 4 +- .../bisq/desktop/main/offer/BuyOfferView.java | 19 +- .../main/offer/MutableOfferDataModel.java | 23 +- .../desktop/main/offer/MutableOfferView.java | 34 +- .../main/offer/MutableOfferViewModel.java | 26 +- .../bisq/desktop/main/offer/OfferView.java | 49 +- .../desktop/main/offer/SellOfferView.java | 19 +- .../createoffer/CreateOfferDataModel.java | 7 +- .../createoffer/CreateOfferViewModel.java | 29 +- .../main/offer/offerbook/OfferBookView.java | 62 +- .../offer/offerbook/OfferBookViewModel.java | 16 +- .../offer/takeoffer/TakeOfferDataModel.java | 38 +- .../main/offer/takeoffer/TakeOfferView.java | 34 +- .../offer/takeoffer/TakeOfferViewModel.java | 12 - .../overlays/notifications/Notification.java | 4 + .../notifications/NotificationCenter.java | 80 +- .../main/overlays/windows/ContractWindow.java | 34 +- .../windows/DisputeSummaryWindow.java | 202 ++-- .../overlays/windows/EmptyWalletWindow.java | 4 +- .../main/overlays/windows/FilterWindow.java | 12 +- .../windows/ManualPayoutTxWindow.java | 4 +- .../overlays/windows/OfferDetailsWindow.java | 17 +- .../overlays/windows/TradeDetailsWindow.java | 28 +- ...UnlockDisputeAgentRegistrationWindow.java} | 6 +- .../closedtrades/ClosedTradesViewModel.java | 10 +- .../editoffer/EditOfferDataModel.java | 7 +- .../editoffer/EditOfferViewModel.java | 29 +- .../portfolio/openoffer/OpenOffersView.java | 24 +- .../openoffer/OpenOffersViewModel.java | 5 +- .../portfolio/pendingtrades/BuyerSubView.java | 5 + .../pendingtrades/PendingTradesDataModel.java | 336 ++++--- .../pendingtrades/PendingTradesView.java | 63 +- .../pendingtrades/SellerSubView.java | 7 +- .../pendingtrades/TradeStepInfo.java | 196 ++++ .../portfolio/pendingtrades/TradeSubView.java | 80 +- .../pendingtrades/steps/TradeStepView.java | 428 +++++--- .../steps/buyer/BuyerStep1View.java | 5 +- .../steps/buyer/BuyerStep2View.java | 37 +- .../steps/buyer/BuyerStep3View.java | 8 +- .../steps/buyer/BuyerStep4View.java | 43 +- .../steps/seller/SellerStep1View.java | 5 +- .../steps/seller/SellerStep2View.java | 5 +- .../steps/seller/SellerStep3View.java | 23 +- .../presentation/MarketPricePresentation.java | 3 +- .../preferences/PreferencesViewModel.java | 6 +- .../{Chat/Chat.java => shared/ChatView.java} | 202 ++-- .../{ => shared}/PriceFeedComboBoxItem.java | 2 +- .../SupportView.fxml} | 5 +- .../desktop/main/support/SupportView.java | 227 +++++ .../dispute/DisputeView.java} | 101 +- .../dispute/agent/DisputeAgentView.java} | 52 +- .../agent/arbitration/ArbitratorView.fxml} | 3 +- .../agent/arbitration/ArbitratorView.java | 78 ++ .../agent/mediation/MediatorView.fxml} | 3 +- .../dispute/agent/mediation/MediatorView.java | 78 ++ .../dispute/client/DisputeClientView.java | 58 ++ .../arbitration/ArbitrationClientView.fxml | 29 + .../arbitration/ArbitrationClientView.java | 70 ++ .../client/mediation/MediationClientView.fxml | 28 + .../client/mediation/MediationClientView.java | 70 ++ .../main/java/bisq/desktop/util/GUIUtil.java | 103 +- .../java/bisq/desktop/GuiceSetupTest.java | 18 + .../TransactionAwareTradableFactoryTest.java | 6 +- .../TransactionAwareTradeTest.java | 12 +- .../createoffer/CreateOfferDataModelTest.java | 2 +- .../createoffer/CreateOfferViewModelTest.java | 9 +- .../offerbook/OfferBookViewModelTest.java | 34 +- .../editoffer/EditOfferDataModelTest.java | 2 +- .../preferences/PreferencesViewModelTest.java | 6 +- .../metric/P2PSeedNodeSnapshotBase.java | 6 +- .../network/p2p/AckMessageSourceType.java | 4 +- .../bisq/network/p2p/BootstrapListener.java | 2 +- .../bisq/network/p2p/network/NetworkNode.java | 4 +- .../storage/persistence/MapStoreService.java | 4 +- .../p2p/storage/persistence/StoreService.java | 4 +- 282 files changed, 9190 insertions(+), 4834 deletions(-) delete mode 100644 core/src/main/java/bisq/core/arbitration/ArbitratorManager.java delete mode 100644 core/src/main/java/bisq/core/arbitration/ArbitratorService.java delete mode 100644 core/src/main/java/bisq/core/arbitration/DisputeChatSession.java delete mode 100644 core/src/main/java/bisq/core/arbitration/DisputeManager.java delete mode 100644 core/src/main/java/bisq/core/chat/ChatManager.java delete mode 100644 core/src/main/java/bisq/core/chat/ChatSession.java rename core/src/main/java/bisq/core/offer/availability/{ArbitratorSelection.java => DisputeAgentSelection.java} (50%) delete mode 100644 core/src/main/java/bisq/core/presentation/DisputePresentation.java create mode 100644 core/src/main/java/bisq/core/presentation/SupportTicketsPresentation.java create mode 100644 core/src/main/java/bisq/core/support/SupportManager.java create mode 100644 core/src/main/java/bisq/core/support/SupportSession.java rename core/src/main/java/bisq/core/{arbitration/ArbitratorModule.java => support/SupportType.java} (57%) rename core/src/main/java/bisq/core/{arbitration => support/dispute}/Attachment.java (97%) rename core/src/main/java/bisq/core/{arbitration => support/dispute}/Dispute.java (89%) rename core/src/main/java/bisq/core/{arbitration => support/dispute}/DisputeAlreadyOpenException.java (95%) rename core/src/main/java/bisq/core/{arbitration => support/dispute}/DisputeList.java (59%) create mode 100644 core/src/main/java/bisq/core/support/dispute/DisputeListService.java create mode 100644 core/src/main/java/bisq/core/support/dispute/DisputeManager.java rename core/src/main/java/bisq/core/{arbitration/MessageDeliveryFailedException.java => support/dispute/DisputeMessageDeliveryFailedException.java} (81%) rename core/src/main/java/bisq/core/{arbitration => support/dispute}/DisputeResult.java (91%) create mode 100644 core/src/main/java/bisq/core/support/dispute/DisputeSession.java create mode 100644 core/src/main/java/bisq/core/support/dispute/agent/DisputeAgent.java create mode 100644 core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentManager.java create mode 100644 core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentService.java create mode 100644 core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeList.java create mode 100644 core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeListService.java create mode 100644 core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java create mode 100644 core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationSession.java rename core/src/main/java/bisq/core/{ => support/dispute}/arbitration/BuyerDataItem.java (94%) rename core/src/main/java/bisq/core/{arbitration => support/dispute/arbitration/arbitrator}/Arbitrator.java (59%) create mode 100644 core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java create mode 100644 core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorService.java create mode 100644 core/src/main/java/bisq/core/support/dispute/arbitration/messages/ArbitrationMessage.java rename core/src/main/java/bisq/core/{ => support/dispute}/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java (78%) create mode 100644 core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeList.java create mode 100644 core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeListService.java create mode 100644 core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java create mode 100644 core/src/main/java/bisq/core/support/dispute/mediation/MediationResultState.java create mode 100644 core/src/main/java/bisq/core/support/dispute/mediation/MediationSession.java rename core/src/main/java/bisq/core/{arbitration => support/dispute/mediation/mediator}/Mediator.java (63%) create mode 100644 core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorManager.java create mode 100644 core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorService.java create mode 100644 core/src/main/java/bisq/core/support/dispute/messages/DisputeMessage.java rename core/src/main/java/bisq/core/{arbitration => support/dispute}/messages/DisputeResultMessage.java (80%) rename core/src/main/java/bisq/core/{arbitration => support/dispute}/messages/OpenNewDisputeMessage.java (81%) rename core/src/main/java/bisq/core/{arbitration => support/dispute}/messages/PeerOpenedDisputeMessage.java (80%) rename core/src/main/java/bisq/core/{arbitration/messages/DisputeCommunicationMessage.java => support/messages/ChatMessage.java} (72%) rename core/src/main/java/bisq/core/{arbitration/messages/DisputeMessage.java => support/messages/SupportMessage.java} (73%) create mode 100644 core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java create mode 100644 core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java delete mode 100644 core/src/main/java/bisq/core/trade/TradeChatSession.java create mode 100644 core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxPublishedMessage.java create mode 100644 core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxSignatureMessage.java delete mode 100644 core/src/main/java/bisq/core/trade/protocol/ArbitratorSelectionRule.java delete mode 100644 core/src/main/java/bisq/core/trade/protocol/MediatorSelectionRule.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/BroadcastPayoutTx.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/SendPayoutTxPublishedMessage.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java rename core/src/main/java/bisq/core/trade/protocol/tasks/{maker/MakerVerifyArbitratorSelection.java => mediation/BroadcastMediatedPayoutTx.java} (54%) create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/mediation/FinalizeMediatedPayoutTx.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutSignatureMessage.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutTxPublishedMessage.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutSignatureMessage.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutTxPublishedMessage.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SetupMediatedPayoutTxListener.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SignMediatedPayoutTx.java delete mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSelectMediator.java rename desktop/src/main/java/bisq/desktop/main/account/{arbitratorregistration/ArbitratorRegistrationView.java => register/AgentRegistrationView.java} (84%) rename desktop/src/main/java/bisq/desktop/main/account/{arbitratorregistration/ArbitratorRegistrationViewModel.java => register/AgentRegistrationViewModel.java} (55%) rename desktop/src/main/java/bisq/desktop/main/account/{arbitratorregistration => register/arbitrator}/ArbitratorRegistrationView.fxml (89%) create mode 100644 desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.java create mode 100644 desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationViewModel.java create mode 100644 desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.fxml create mode 100644 desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.java create mode 100644 desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationViewModel.java delete mode 100644 desktop/src/main/java/bisq/desktop/main/disputes/DisputesView.java rename desktop/src/main/java/bisq/desktop/main/overlays/windows/{UnlockArbitrationRegistrationWindow.java => UnlockDisputeAgentRegistrationWindow.java} (94%) create mode 100644 desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeStepInfo.java rename desktop/src/main/java/bisq/desktop/main/{Chat/Chat.java => shared/ChatView.java} (80%) rename desktop/src/main/java/bisq/desktop/main/{ => shared}/PriceFeedComboBoxItem.java (97%) rename desktop/src/main/java/bisq/desktop/main/{disputes/DisputesView.fxml => support/SupportView.fxml} (83%) create mode 100644 desktop/src/main/java/bisq/desktop/main/support/SupportView.java rename desktop/src/main/java/bisq/desktop/main/{disputes/trader/TraderDisputeView.java => support/dispute/DisputeView.java} (92%) rename desktop/src/main/java/bisq/desktop/main/{disputes/arbitrator/ArbitratorDisputeView.java => support/dispute/agent/DisputeAgentView.java} (63%) rename desktop/src/main/java/bisq/desktop/main/{disputes/trader/TraderDisputeView.fxml => support/dispute/agent/arbitration/ArbitratorView.fxml} (90%) create mode 100644 desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java rename desktop/src/main/java/bisq/desktop/main/{disputes/arbitrator/ArbitratorDisputeView.fxml => support/dispute/agent/mediation/MediatorView.fxml} (90%) create mode 100644 desktop/src/main/java/bisq/desktop/main/support/dispute/agent/mediation/MediatorView.java create mode 100644 desktop/src/main/java/bisq/desktop/main/support/dispute/client/DisputeClientView.java create mode 100644 desktop/src/main/java/bisq/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.fxml create mode 100644 desktop/src/main/java/bisq/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.java create mode 100644 desktop/src/main/java/bisq/desktop/main/support/dispute/client/mediation/MediationClientView.fxml create mode 100644 desktop/src/main/java/bisq/desktop/main/support/dispute/client/mediation/MediationClientView.java diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index 79ee123c2b..6e6eec1148 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,6 @@ \ No newline at end of file diff --git a/common/src/main/java/bisq/common/app/Capabilities.java b/common/src/main/java/bisq/common/app/Capabilities.java index d87af5cd64..7af63477c0 100644 --- a/common/src/main/java/bisq/common/app/Capabilities.java +++ b/common/src/main/java/bisq/common/app/Capabilities.java @@ -17,10 +17,14 @@ package bisq.common.app; +import com.google.common.base.Joiner; + import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -43,7 +47,7 @@ public class Capabilities { // Defines which most recent capability any node need to support. // This helps to clean network from very old inactive but still running nodes. - private static final Capability mandatoryCapability = Capability.DAO_STATE; + private static final Capability MANDATORY_CAPABILITY = Capability.DAO_STATE; protected final Set capabilities = new HashSet<>(); @@ -101,7 +105,7 @@ public class Capabilities { * @return int list of Capability ordinals */ public static List toIntList(Capabilities capabilities) { - return capabilities.capabilities.stream().map(capability -> capability.ordinal()).sorted().collect(Collectors.toList()); + return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList()); } /** @@ -113,11 +117,46 @@ public class Capabilities { public static Capabilities fromIntList(List capabilities) { return new Capabilities(capabilities.stream() .filter(integer -> integer < Capability.values().length) + .filter(integer -> integer >= 0) .map(integer -> Capability.values()[integer]) .collect(Collectors.toSet())); } + /** + * + * @param list Comma separated list of Capability ordinals. + * @return Capabilities + */ + public static Capabilities fromStringList(String list) { + if (list == null || list.isEmpty()) + return new Capabilities(); + + List entries = List.of(list.replace(" ", "").split(",")); + List capabilitiesList = entries.stream() + .map(c -> { + try { + return Integer.parseInt(c); + } catch (Throwable e) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + return Capabilities.fromIntList(capabilitiesList); + } + + /** + * @return Converts capabilities to list of ordinals as comma separated strings + */ + public String toStringList() { + return Joiner.on(", ").join(Capabilities.toIntList(this)); + } + public static boolean hasMandatoryCapability(Capabilities capabilities) { + return hasMandatoryCapability(capabilities, MANDATORY_CAPABILITY); + } + + public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) { return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability); } @@ -126,6 +165,13 @@ public class Capabilities { return Arrays.toString(Capabilities.toIntList(this).toArray()); } + public String prettyPrint() { + return capabilities.stream() + .sorted(Comparator.comparingInt(Enum::ordinal)) + .map(e -> e.name() + " [" + e.ordinal() + "]") + .collect(Collectors.joining(", ")); + } + public int size() { return capabilities.size(); } diff --git a/common/src/main/java/bisq/common/app/Capability.java b/common/src/main/java/bisq/common/app/Capability.java index 9dcc8ae989..8d18736f7b 100644 --- a/common/src/main/java/bisq/common/app/Capability.java +++ b/common/src/main/java/bisq/common/app/Capability.java @@ -20,7 +20,8 @@ package bisq.common.app; // We can define here special features the client is supporting. // Useful for updates to new versions where a new data type would break backwards compatibility or to // limit a node to certain behaviour and roles like the seed nodes. -// We don't use the Enum in any serialized data, as changes in the enum would break backwards compatibility. We use the ordinal integer instead. +// We don't use the Enum in any serialized data, as changes in the enum would break backwards compatibility. +// We use the ordinal integer instead. // Sequence in the enum must not be changed (append only). public enum Capability { @Deprecated TRADE_STATISTICS, // Not required anymore as no old clients out there not having that support @@ -37,5 +38,6 @@ public enum Capability { //TODO can be set deprecated after v1.1.6 as we enforce update there BUNDLE_OF_ENVELOPES, // Supports bundling of messages if many messages are sent in short interval - SIGNED_ACCOUNT_AGE_WITNESS // Supports the signed account age witness feature + SIGNED_ACCOUNT_AGE_WITNESS, // Supports the signed account age witness feature + MEDIATION // Supports mediation feature } diff --git a/common/src/main/java/bisq/common/app/DevEnv.java b/common/src/main/java/bisq/common/app/DevEnv.java index 14429b7a3b..5cf6132cf9 100644 --- a/common/src/main/java/bisq/common/app/DevEnv.java +++ b/common/src/main/java/bisq/common/app/DevEnv.java @@ -41,7 +41,6 @@ public class DevEnv { // If set to true we ignore several UI behavior like confirmation popups as well dummy accounts are created and // offers are filled with default values. Intended to make dev testing faster. - @SuppressWarnings("PointlessBooleanExpression") private static boolean devMode = false; public static boolean isDevMode() { diff --git a/common/src/main/java/bisq/common/taskrunner/Task.java b/common/src/main/java/bisq/common/taskrunner/Task.java index 98aad1d93c..e6fce41550 100644 --- a/common/src/main/java/bisq/common/taskrunner/Task.java +++ b/common/src/main/java/bisq/common/taskrunner/Task.java @@ -35,7 +35,7 @@ public abstract class Task { this.model = model; } - abstract protected void run(); + protected abstract void run(); protected void runInterceptHook() { if (getClass() == taskToIntercept) diff --git a/common/src/main/java/bisq/common/util/MathUtils.java b/common/src/main/java/bisq/common/util/MathUtils.java index 78b787843a..cb3a32a9c7 100644 --- a/common/src/main/java/bisq/common/util/MathUtils.java +++ b/common/src/main/java/bisq/common/util/MathUtils.java @@ -93,7 +93,11 @@ public class MathUtils { return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue(); } - public static Long getMedian(Long[] list) { + public static long getMedian(Long[] list) { + if (list.length == 0) { + return 0L; + } + int middle = list.length / 2; long median; if (list.length % 2 == 1) { diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index 7742774b49..503d0d9e02 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -44,7 +44,7 @@ message NetworkEnvelope { OpenNewDisputeMessage open_new_dispute_message = 22; PeerOpenedDisputeMessage peer_opened_dispute_message = 23; - DisputeCommunicationMessage dispute_communication_message = 24; + ChatMessage chat_message = 24; DisputeResultMessage dispute_result_message = 25; PeerPublishedDisputePayoutTxMessage peer_published_dispute_payout_tx_message = 26; @@ -68,6 +68,8 @@ message NetworkEnvelope { GetBlindVoteStateHashesResponse get_blind_vote_state_hashes_response = 42; BundleOfEnvelopes bundle_of_envelopes = 43; + MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 44; + MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 45; } } @@ -141,6 +143,7 @@ message OfferAvailabilityResponse { repeated int32 supported_capabilities = 3; string uid = 4; NodeAddress arbitrator = 5; + NodeAddress mediator = 6; } message RefreshOfferMessage { @@ -267,26 +270,43 @@ message PayoutTxPublishedMessage { string uid = 4; } +message MediatedPayoutTxPublishedMessage { + string trade_id = 1; + bytes payout_tx = 2; + NodeAddress sender_node_address = 3; + string uid = 4; +} + +message MediatedPayoutTxSignatureMessage { + string uid = 1; + bytes tx_signature = 2; + string trade_id = 3; + NodeAddress sender_node_address = 4; +} + // dispute +enum SupportType { + ARBITRATION = 0; + MEDIATION = 1; + TRADE = 2; +} + message OpenNewDisputeMessage { Dispute dispute = 1; NodeAddress sender_node_address = 2; string uid = 3; + SupportType type = 4; } message PeerOpenedDisputeMessage { Dispute dispute = 1; NodeAddress sender_node_address = 2; string uid = 3; + SupportType type = 4; } -message DisputeCommunicationMessage { - enum Type { - MEDIATION = 0; - ARBITRATION = 1; - TRADE = 2; - } +message ChatMessage { int64 date = 1; string trade_id = 2; int32 trader_id = 3; @@ -301,7 +321,7 @@ message DisputeCommunicationMessage { string send_message_error = 12; bool acknowledged = 13; string ack_error = 14; - Type type = 15; + SupportType type = 15; bool was_displayed = 16; } @@ -309,6 +329,7 @@ message DisputeResultMessage { string uid = 1; DisputeResult dispute_result = 2; NodeAddress sender_node_address = 3; + SupportType type = 4; } message PeerPublishedDisputePayoutTxMessage { @@ -316,9 +337,9 @@ message PeerPublishedDisputePayoutTxMessage { bytes transaction = 2; string trade_id = 3; NodeAddress sender_node_address = 4; + SupportType type = 5; } - message PrivateNotificationMessage { string uid = 1; NodeAddress sender_node_address = 2; @@ -546,6 +567,7 @@ message Filter { bool disable_dao = 14; string disable_dao_below_version = 15; string disable_trade_below_version = 16; + repeated string mediators = 17; } // not used anymore from v0.6 on. But leave it for receiving TradeStatistics objects from older @@ -614,8 +636,8 @@ message OfferPayload { int64 min_amount = 10; string base_currency_code = 11; string counter_currency_code = 12; - repeated NodeAddress arbitrator_node_addresses = 13; - repeated NodeAddress mediator_node_addresses = 14; + repeated NodeAddress arbitrator_node_addresses = 13 [deprecated = true]; // not used anymore but still required as old clients check for nonNull + repeated NodeAddress mediator_node_addresses = 14 [deprecated = true]; // not used anymore but still required as old clients check for nonNull string payment_method_id = 15; string maker_payment_account_id = 16; string offer_fee_payment_tx_id = 17; @@ -680,9 +702,9 @@ message Dispute { string contract_as_json = 15; string maker_contract_signature = 16; string taker_contract_signature = 17; - PubKeyRing arbitrator_pub_key_ring = 18; + PubKeyRing agent_pub_key_ring = 18; bool is_support_ticket = 19; - repeated DisputeCommunicationMessage dispute_communication_messages = 20; + repeated ChatMessage chat_message = 20; bool is_closed = 21; DisputeResult dispute_result = 22; string dispute_payout_tx_id = 23; @@ -719,7 +741,7 @@ message DisputeResult { bool id_verification = 6; bool screen_cast = 7; string summary_notes = 8; - DisputeCommunicationMessage dispute_communication_message = 9; + ChatMessage chat_message = 9; bytes arbitrator_signature = 10; int64 buyer_payout_amount = 11; int64 seller_payout_amount = 12; @@ -770,6 +792,7 @@ enum AvailabilityResult { NO_ARBITRATORS = 6; NO_MEDIATORS = 7; USER_IGNORED = 8; + MISSING_MANDATORY_CAPABILITY = 9; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -1009,7 +1032,7 @@ message PersistableEnvelope { TradableList tradable_list = 6; TradeStatisticsList trade_statistics_list = 7 [deprecated = true]; // Was used in pre v0.6.0 version. Not used anymore. - DisputeList dispute_list = 8; + ArbitrationDisputeList arbitration_dispute_list = 8; PreferencesPayload preferences_payload = 9; UserPayload user_payload = 10; @@ -1039,6 +1062,7 @@ message PersistableEnvelope { MyProofOfBurnList my_proof_of_burn_list = 26; UnconfirmedBsqChangeOutputList unconfirmed_bsq_change_output_list = 27; SignedWitnessStore signed_witness_store = 28; + MediationDisputeList mediation_dispute_list = 29; } } @@ -1162,6 +1186,7 @@ message OpenOffer { Offer offer = 1; State state = 2; NodeAddress arbitrator_node_address = 3; + NodeAddress mediator_node_address = 4; } message Tradable { @@ -1224,9 +1249,12 @@ message Trade { enum DisputeState { PB_ERROR_DISPUTE_STATE = 0; NO_DISPUTE = 1; - DISPUTE_REQUESTED = 2; - DISPUTE_STARTED_BY_PEER = 3; - DISPUTE_CLOSED = 4; + DISPUTE_REQUESTED = 2; // arbitration We use the enum name for resolving enums so it cannot be renamed + DISPUTE_STARTED_BY_PEER = 3; // arbitration We use the enum name for resolving enums so it cannot be renamed + DISPUTE_CLOSED = 4; // arbitration We use the enum name for resolving enums so it cannot be renamed + MEDIATION_REQUESTED = 5; + MEDIATION_STARTED_BY_PEER = 6; + MEDIATION_CLOSED = 7; } enum TradePeriodState { @@ -1264,7 +1292,8 @@ message Trade { PubKeyRing arbitrator_pub_key_ring = 26; PubKeyRing mediator_pub_key_ring = 27; string counter_currency_tx_id = 28; - repeated DisputeCommunicationMessage communication_messages = 29; + repeated ChatMessage chat_message = 29; + MediationResultState mediation_result_state = 30; } message BuyerAsMakerTrade { @@ -1301,6 +1330,9 @@ message ProcessModel { bytes my_multi_sig_pub_key = 15; NodeAddress temp_trading_peer_node_address = 16; string payment_started_message_state = 17; + bytes mediated_payout_tx_signature = 18; + int64 buyer_payout_amount_from_mediation = 19; + int64 seller_payout_amount_from_mediation = 20; } message TradingPeer { @@ -1318,16 +1350,40 @@ message TradingPeer { bytes account_age_witness_nonce = 12; bytes account_age_witness_signature = 13; int64 current_date = 14; + bytes mediated_payout_tx_signature = 15; } /////////////////////////////////////////////////////////////////////////////////////////// // Dispute /////////////////////////////////////////////////////////////////////////////////////////// -message DisputeList { +message ArbitrationDisputeList { repeated Dispute dispute = 1; } +message MediationDisputeList { + repeated Dispute dispute = 1; +} + +enum MediationResultState { + PB_ERROR_MEDIATION_RESULT = 0; + UNDEFINED_MEDIATION_RESULT = 1; + MEDIATION_RESULT_ACCEPTED = 2; + MEDIATION_RESULT_REJECTED = 3; + SIG_MSG_SENT = 4; + SIG_MSG_ARRIVED = 5; + SIG_MSG_IN_MAILBOX = 6; + SIG_MSG_SEND_FAILED = 7; + RECEIVED_SIG_MSG = 8; + PAYOUT_TX_PUBLISHED = 9; + PAYOUT_TX_PUBLISHED_MSG_SENT = 10; + PAYOUT_TX_PUBLISHED_MSG_ARRIVED = 11; + PAYOUT_TX_PUBLISHED_MSG_IN_MAILBOX = 12; + PAYOUT_TX_PUBLISHED_MSG_SEND_FAILED = 13; + RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 14; + PAYOUT_TX_SEEN_IN_NETWORK = 15; +} + /////////////////////////////////////////////////////////////////////////////////////////// // Preferences /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/common/src/test/java/bisq/common/app/CapabilitiesTest.java b/common/src/test/java/bisq/common/app/CapabilitiesTest.java index d0166e130c..735fc899ea 100644 --- a/common/src/test/java/bisq/common/app/CapabilitiesTest.java +++ b/common/src/test/java/bisq/common/app/CapabilitiesTest.java @@ -17,11 +17,16 @@ package bisq.common.app; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import org.junit.Test; -import static bisq.common.app.Capability.*; +import static bisq.common.app.Capability.SEED_NODE; +import static bisq.common.app.Capability.TRADE_STATISTICS; +import static bisq.common.app.Capability.TRADE_STATISTICS_2; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -60,4 +65,47 @@ public class CapabilitiesTest { assertTrue(DUT.containsAll(new Capabilities(TRADE_STATISTICS, TRADE_STATISTICS_2))); assertFalse(DUT.containsAll(new Capabilities(SEED_NODE, TRADE_STATISTICS_2))); } + + @Test + public void testToIntList() { + assertEquals(Collections.emptyList(), Capabilities.toIntList(new Capabilities())); + assertEquals(Collections.singletonList(12), Capabilities.toIntList(new Capabilities(Capability.MEDIATION))); + assertEquals(Arrays.asList(6, 12), Capabilities.toIntList(new Capabilities(Capability.MEDIATION, Capability.BLIND_VOTE))); + } + + @Test + public void testFromIntList() { + assertEquals(new Capabilities(), Capabilities.fromIntList(Collections.emptyList())); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromIntList(Collections.singletonList(12))); + assertEquals(new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION), Capabilities.fromIntList(Arrays.asList(6, 12))); + + assertEquals(new Capabilities(), Capabilities.fromIntList(Collections.singletonList(-1))); + assertEquals(new Capabilities(), Capabilities.fromIntList(Collections.singletonList(99))); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromIntList(Arrays.asList(-6, 12))); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromIntList(Arrays.asList(12, 99))); + } + + @Test + public void testToStringList() { + assertEquals("", new Capabilities().toStringList()); + assertEquals("12", new Capabilities(Capability.MEDIATION).toStringList()); + assertEquals("6, 12", new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION).toStringList()); + // capabilities gets sorted, independent of our order + assertEquals("6, 12", new Capabilities(Capability.MEDIATION, Capability.BLIND_VOTE).toStringList()); + } + + @Test + public void testFromStringList() { + assertEquals(new Capabilities(), Capabilities.fromStringList(null)); + assertEquals(new Capabilities(), Capabilities.fromStringList("")); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("12")); + assertEquals(new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION), Capabilities.fromStringList("6,12")); + assertEquals(new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION), Capabilities.fromStringList("12, 6")); + assertEquals(new Capabilities(), Capabilities.fromStringList("a")); + assertEquals(new Capabilities(), Capabilities.fromStringList("99")); + assertEquals(new Capabilities(), Capabilities.fromStringList("-1")); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("12, a")); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("12, 99")); + assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("a,12, 99")); + } } diff --git a/core/src/main/java/bisq/core/CoreModule.java b/core/src/main/java/bisq/core/CoreModule.java index b73c25dd2c..3e5812bbe3 100644 --- a/core/src/main/java/bisq/core/CoreModule.java +++ b/core/src/main/java/bisq/core/CoreModule.java @@ -20,7 +20,6 @@ package bisq.core; import bisq.core.alert.AlertModule; import bisq.core.app.AppOptionKeys; import bisq.core.app.BisqEnvironment; -import bisq.core.arbitration.ArbitratorModule; import bisq.core.btc.BitcoinModule; import bisq.core.dao.DaoModule; import bisq.core.filter.FilterModule; @@ -90,7 +89,6 @@ public class CoreModule extends AppModule { // ordering is used for shut down sequence install(tradeModule()); install(encryptionServiceModule()); - install(arbitratorModule()); install(offerModule()); install(p2pModule()); install(bitcoinModule()); @@ -109,10 +107,6 @@ public class CoreModule extends AppModule { return new EncryptionServiceModule(environment); } - private ArbitratorModule arbitratorModule() { - return new ArbitratorModule(environment); - } - private AlertModule alertModule() { return new AlertModule(environment); } diff --git a/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java index 0c9f6d49c6..ef87a6e35f 100644 --- a/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java +++ b/core/src/main/java/bisq/core/account/sign/SignedWitnessService.java @@ -19,14 +19,14 @@ package bisq.core.account.sign; import bisq.core.account.witness.AccountAgeWitness; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.arbitration.ArbitratorManager; -import bisq.core.arbitration.BuyerDataItem; -import bisq.core.arbitration.Dispute; -import bisq.core.arbitration.DisputeManager; -import bisq.core.arbitration.DisputeResult; import bisq.core.payment.ChargeBackRisk; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.arbitration.BuyerDataItem; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.network.p2p.P2PService; import bisq.network.p2p.storage.P2PDataStorage; @@ -75,7 +75,7 @@ public class SignedWitnessService { private final P2PService p2PService; private final AccountAgeWitnessService accountAgeWitnessService; private final ArbitratorManager arbitratorManager; - private final DisputeManager disputeManager; + private final ArbitrationManager arbitrationManager; private final ChargeBackRisk chargeBackRisk; private final Map signedWitnessMap = new HashMap<>(); @@ -92,13 +92,13 @@ public class SignedWitnessService { ArbitratorManager arbitratorManager, SignedWitnessStorageService signedWitnessStorageService, AppendOnlyDataStoreService appendOnlyDataStoreService, - DisputeManager disputeManager, + ArbitrationManager arbitrationManager, ChargeBackRisk chargeBackRisk) { this.keyRing = keyRing; this.p2PService = p2PService; this.accountAgeWitnessService = accountAgeWitnessService; this.arbitratorManager = arbitratorManager; - this.disputeManager = disputeManager; + this.arbitrationManager = arbitrationManager; this.chargeBackRisk = chargeBackRisk; // We need to add that early (before onAllServicesInitialized) as it will be used at startup. @@ -333,7 +333,7 @@ public class SignedWitnessService { // Arbitrator signing public List getBuyerPaymentAccounts(long safeDate, PaymentMethod paymentMethod) { - return disputeManager.getDisputesAsObservableList().stream() + return arbitrationManager.getDisputesAsObservableList().stream() .filter(dispute -> dispute.getContract().getPaymentMethodId().equals(paymentMethod.getId())) .filter(this::hasChargebackRisk) .filter(this::isBuyerWinner) diff --git a/core/src/main/java/bisq/core/alert/Alert.java b/core/src/main/java/bisq/core/alert/Alert.java index ea063c4e6f..080c827251 100644 --- a/core/src/main/java/bisq/core/alert/Alert.java +++ b/core/src/main/java/bisq/core/alert/Alert.java @@ -99,7 +99,7 @@ public final class Alert implements ProtectedStoragePayload, ExpirablePayload { public protobuf.StoragePayload toProtoMessage() { checkNotNull(ownerPubKeyBytes, "storagePublicKeyBytes must not be null"); checkNotNull(signatureAsBase64, "signatureAsBase64 must not be null"); - final protobuf.Alert.Builder builder = protobuf.Alert.newBuilder() + protobuf.Alert.Builder builder = protobuf.Alert.newBuilder() .setMessage(message) .setIsUpdateInfo(isUpdateInfo) .setVersion(version) @@ -109,7 +109,13 @@ public final class Alert implements ProtectedStoragePayload, ExpirablePayload { return protobuf.StoragePayload.newBuilder().setAlert(builder).build(); } + @Nullable public static Alert fromProto(protobuf.Alert proto) { + // We got in dev testing sometimes an empty protobuf Alert. Not clear why that happened but as it causes an + // exception and corrupted user db file we prefer to set it to null. + if (proto.getSignatureAsBase64().isEmpty()) + return null; + return new Alert(proto.getMessage(), proto.getIsUpdateInfo(), proto.getVersion(), diff --git a/core/src/main/java/bisq/core/app/BisqExecutable.java b/core/src/main/java/bisq/core/app/BisqExecutable.java index e704af407f..f5935a5406 100644 --- a/core/src/main/java/bisq/core/app/BisqExecutable.java +++ b/core/src/main/java/bisq/core/app/BisqExecutable.java @@ -17,7 +17,6 @@ package bisq.core.app; -import bisq.core.arbitration.ArbitratorManager; import bisq.core.btc.BtcOptionKeys; import bisq.core.btc.setup.RegTestHost; import bisq.core.btc.setup.WalletsSetup; @@ -29,6 +28,7 @@ import bisq.core.exceptions.BisqException; import bisq.core.offer.OpenOfferManager; import bisq.core.setup.CorePersistedDataHost; import bisq.core.setup.CoreSetup; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.trade.TradeManager; import bisq.network.NetworkOptionKeys; @@ -242,11 +242,11 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet try { PersistedDataHost.apply(CorePersistedDataHost.getPersistedDataHosts(injector)); } catch (Throwable t) { + log.error("Error at PersistedDataHost.apply: {}", t.toString(), t); // If we are in dev mode we want to get the exception if some db files are corrupted // We need to delay it as the stage is not created yet and so popups would not be shown. if (DevEnv.isDevMode()) UserThread.runAfter(() -> { - log.error("Error at PersistedDataHost.apply: {}", t.toString()); throw t; }, 2); } diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index 44e0d15245..a77eafa876 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -23,8 +23,6 @@ import bisq.core.alert.Alert; import bisq.core.alert.AlertManager; import bisq.core.alert.PrivateNotificationManager; import bisq.core.alert.PrivateNotificationPayload; -import bisq.core.arbitration.ArbitratorManager; -import bisq.core.arbitration.DisputeManager; import bisq.core.btc.Balances; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.setup.WalletsSetup; @@ -47,6 +45,11 @@ import bisq.core.payment.PaymentAccount; import bisq.core.payment.TradeLimits; import bisq.core.provider.fee.FeeService; import bisq.core.provider.price.PriceFeedService; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.traderchat.TraderChatManager; import bisq.core.trade.TradeManager; import bisq.core.trade.statistics.AssetTradeActivityCheck; import bisq.core.trade.statistics.TradeStatisticsManager; @@ -127,10 +130,13 @@ public class BisqSetup { private final Balances balances; private final PriceFeedService priceFeedService; private final ArbitratorManager arbitratorManager; + private final MediatorManager mediatorManager; private final P2PService p2PService; private final TradeManager tradeManager; private final OpenOfferManager openOfferManager; - private final DisputeManager disputeManager; + private final ArbitrationManager arbitrationManager; + private final MediationManager mediationManager; + private final TraderChatManager traderChatManager; private final Preferences preferences; private final User user; private final AlertManager alertManager; @@ -206,10 +212,13 @@ public class BisqSetup { Balances balances, PriceFeedService priceFeedService, ArbitratorManager arbitratorManager, + MediatorManager mediatorManager, P2PService p2PService, TradeManager tradeManager, OpenOfferManager openOfferManager, - DisputeManager disputeManager, + ArbitrationManager arbitrationManager, + MediationManager mediationManager, + TraderChatManager traderChatManager, Preferences preferences, User user, AlertManager alertManager, @@ -247,10 +256,13 @@ public class BisqSetup { this.balances = balances; this.priceFeedService = priceFeedService; this.arbitratorManager = arbitratorManager; + this.mediatorManager = mediatorManager; this.p2PService = p2PService; this.tradeManager = tradeManager; this.openOfferManager = openOfferManager; - this.disputeManager = disputeManager; + this.arbitrationManager = arbitrationManager; + this.mediationManager = mediationManager; + this.traderChatManager = traderChatManager; this.preferences = preferences; this.user = user; this.alertManager = alertManager; @@ -586,12 +598,15 @@ public class BisqSetup { .filter(e -> tradeManager.getSetOfAllTradeIds().contains(e.getOfferId()) && e.getContext() == AddressEntry.Context.MULTI_SIG) .forEach(e -> { - final Coin balance = e.getCoinLockedInMultiSig(); - final String message = Res.get("popup.warning.lockedUpFunds", - formatter.formatCoinWithCode(balance), e.getAddressString(), e.getOfferId()); - log.warn(message); - if (lockedUpFundsHandler != null) - lockedUpFundsHandler.accept(message); + Coin balance = e.getCoinLockedInMultiSig(); + if (balance.isPositive()) { + String message = Res.get("popup.warning.lockedUpFunds", + formatter.formatCoinWithCode(balance), e.getAddressString(), e.getOfferId()); + log.warn(message); + if (lockedUpFundsHandler != null) { + lockedUpFundsHandler.accept(message); + } + } }); } @@ -614,7 +629,9 @@ public class BisqSetup { tradeLimits.onAllServicesInitialized(); - disputeManager.onAllServicesInitialized(); + arbitrationManager.onAllServicesInitialized(); + mediationManager.onAllServicesInitialized(); + traderChatManager.onAllServicesInitialized(); tradeManager.onAllServicesInitialized(); @@ -626,6 +643,7 @@ public class BisqSetup { balances.onAllServicesInitialized(); arbitratorManager.onAllServicesInitialized(); + mediatorManager.onAllServicesInitialized(); alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) -> displayAlertIfPresent(newValue, false)); diff --git a/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java index 55e41da382..5959b7ee89 100644 --- a/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java +++ b/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java @@ -20,11 +20,11 @@ package bisq.core.app.misc; import bisq.core.app.AppOptionKeys; import bisq.core.app.BisqEnvironment; import bisq.core.app.BisqExecutable; -import bisq.core.arbitration.ArbitratorManager; import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.OpenOfferManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.network.p2p.P2PService; diff --git a/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java b/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java index 517107e311..bf9050f271 100644 --- a/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java +++ b/core/src/main/java/bisq/core/app/misc/ModuleForAppWithP2p.java @@ -21,7 +21,6 @@ import bisq.core.alert.AlertModule; import bisq.core.app.AppOptionKeys; import bisq.core.app.BisqEnvironment; import bisq.core.app.TorSetup; -import bisq.core.arbitration.ArbitratorModule; import bisq.core.btc.BitcoinModule; import bisq.core.dao.DaoModule; import bisq.core.filter.FilterModule; @@ -98,7 +97,6 @@ public class ModuleForAppWithP2p extends AppModule { // ordering is used for shut down sequence install(tradeModule()); install(encryptionServiceModule()); - install(arbitratorModule()); install(offerModule()); install(p2pModule()); install(bitcoinModule()); @@ -121,10 +119,6 @@ public class ModuleForAppWithP2p extends AppModule { return new EncryptionServiceModule(environment); } - protected ArbitratorModule arbitratorModule() { - return new ArbitratorModule(environment); - } - protected AlertModule alertModule() { return new AlertModule(environment); } diff --git a/core/src/main/java/bisq/core/arbitration/ArbitratorManager.java b/core/src/main/java/bisq/core/arbitration/ArbitratorManager.java deleted file mode 100644 index f9809d6c48..0000000000 --- a/core/src/main/java/bisq/core/arbitration/ArbitratorManager.java +++ /dev/null @@ -1,375 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - -package bisq.core.arbitration; - -import bisq.core.app.AppOptionKeys; -import bisq.core.filter.FilterManager; -import bisq.core.user.Preferences; -import bisq.core.user.User; - -import bisq.network.p2p.BootstrapListener; -import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.P2PService; -import bisq.network.p2p.storage.HashMapChangedListener; -import bisq.network.p2p.storage.payload.ProtectedStorageEntry; - -import bisq.common.Timer; -import bisq.common.UserThread; -import bisq.common.app.DevEnv; -import bisq.common.crypto.KeyRing; -import bisq.common.handlers.ErrorMessageHandler; -import bisq.common.handlers.ResultHandler; -import bisq.common.util.Utilities; - -import org.bitcoinj.core.ECKey; -import org.bitcoinj.core.Utils; - -import com.google.inject.Inject; -import com.google.inject.name.Named; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableMap; - -import java.security.PublicKey; -import java.security.SignatureException; - -import java.math.BigInteger; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.function.Function; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.jetbrains.annotations.NotNull; - -import javax.annotation.Nullable; - -import static org.bitcoinj.core.Utils.HEX; - -public class ArbitratorManager { - private static final Logger log = LoggerFactory.getLogger(ArbitratorManager.class); - - /////////////////////////////////////////////////////////////////////////////////////////// - // Static - /////////////////////////////////////////////////////////////////////////////////////////// - - private static final long REPUBLISH_MILLIS = Arbitrator.TTL / 2; - private static final long RETRY_REPUBLISH_SEC = 5; - private static final long REPEATED_REPUBLISH_AT_STARTUP_SEC = 60; - - private final List publicKeys; - - /////////////////////////////////////////////////////////////////////////////////////////// - // Instance fields - /////////////////////////////////////////////////////////////////////////////////////////// - - private final KeyRing keyRing; - private final ArbitratorService arbitratorService; - private final User user; - private final Preferences preferences; - private final FilterManager filterManager; - private final ObservableMap arbitratorsObservableMap = FXCollections.observableHashMap(); - private List persistedAcceptedArbitrators; - private Timer republishArbitratorTimer, retryRepublishArbitratorTimer; - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor - /////////////////////////////////////////////////////////////////////////////////////////// - - @Inject - public ArbitratorManager(KeyRing keyRing, - ArbitratorService arbitratorService, - User user, - Preferences preferences, - FilterManager filterManager, - @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { - this.keyRing = keyRing; - this.arbitratorService = arbitratorService; - this.user = user; - this.preferences = preferences; - this.filterManager = filterManager; - publicKeys = useDevPrivilegeKeys ? - Collections.unmodifiableList(Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY)) : - Collections.unmodifiableList(Arrays.asList( - "0365c6af94681dbee69de1851f98d4684063bf5c2d64b1c73ed5d90434f375a054", - "031c502a60f9dbdb5ae5e438a79819e4e1f417211dd537ac12c9bc23246534c4bd", - "02c1e5a242387b6d5319ce27246cea6edaaf51c3550591b528d2578a4753c56c2c", - "025c319faf7067d9299590dd6c97fe7e56cd4dac61205ccee1cd1fc390142390a2", - "038f6e24c2bfe5d51d0a290f20a9a657c270b94ef2b9c12cd15ca3725fa798fc55", - "0255256ff7fb615278c4544a9bbd3f5298b903b8a011cd7889be19b6b1c45cbefe", - "024a3a37289f08c910fbd925ebc72b946f33feaeff451a4738ee82037b4cda2e95", - "02a88b75e9f0f8afba1467ab26799dcc38fd7a6468fb2795444b425eb43e2c10bd", - "02349a51512c1c04c67118386f4d27d768c5195a83247c150a4b722d161722ba81", - "03f718a2e0dc672c7cdec0113e72c3322efc70412bb95870750d25c32cd98de17d", - "028ff47ee2c56e66313928975c58fa4f1b19a0f81f3a96c4e9c9c3c6768075509e", - "02b517c0cbc3a49548f448ddf004ed695c5a1c52ec110be1bfd65fa0ca0761c94b", - "03df837a3a0f3d858e82f3356b71d1285327f101f7c10b404abed2abc1c94e7169", - "0203a90fb2ab698e524a5286f317a183a84327b8f8c3f7fa4a98fec9e1cefd6b72", - "023c99cc073b851c892d8c43329ca3beb5d2213ee87111af49884e3ce66cbd5ba5" - )); - } - - public void shutDown() { - stopRepublishArbitratorTimer(); - stopRetryRepublishArbitratorTimer(); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// - - public void onAllServicesInitialized() { - arbitratorService.addHashSetChangedListener(new HashMapChangedListener() { - @Override - public void onAdded(ProtectedStorageEntry data) { - if (data.getProtectedStoragePayload() instanceof Arbitrator) - updateArbitratorMap(); - } - - @Override - public void onRemoved(ProtectedStorageEntry data) { - if (data.getProtectedStoragePayload() instanceof Arbitrator) { - updateArbitratorMap(); - final Arbitrator arbitrator = (Arbitrator) data.getProtectedStoragePayload(); - user.removeAcceptedArbitrator(arbitrator); - user.removeAcceptedMediator(getMediator(arbitrator)); - } - } - }); - - persistedAcceptedArbitrators = new ArrayList<>(user.getAcceptedArbitrators()); - user.clearAcceptedArbitrators(); - - // TODO we mirror arbitrator data for mediator as long we have not impl. it in the UI - user.clearAcceptedMediators(); - - if (user.getRegisteredArbitrator() != null) { - P2PService p2PService = arbitratorService.getP2PService(); - if (p2PService.isBootstrapped()) - startRepublishArbitrator(); - else - p2PService.addP2PServiceListener(new BootstrapListener() { - @Override - public void onUpdatedDataReceived() { - startRepublishArbitrator(); - } - }); - } - - filterManager.filterProperty().addListener((observable, oldValue, newValue) -> updateArbitratorMap()); - - updateArbitratorMap(); - } - - private void startRepublishArbitrator() { - if (republishArbitratorTimer == null) { - republishArbitratorTimer = UserThread.runPeriodically(this::republishArbitrator, REPUBLISH_MILLIS, TimeUnit.MILLISECONDS); - UserThread.runAfter(this::republishArbitrator, REPEATED_REPUBLISH_AT_STARTUP_SEC); - republishArbitrator(); - } - } - - public void updateArbitratorMap() { - Map map = arbitratorService.getArbitrators(); - arbitratorsObservableMap.clear(); - Map filtered = map.values().stream() - .filter(e -> { - final String pubKeyAsHex = Utils.HEX.encode(e.getRegistrationPubKey()); - final boolean isInPublicKeyInList = isPublicKeyInList(pubKeyAsHex); - if (!isInPublicKeyInList) { - if (DevEnv.DEV_PRIVILEGE_PUB_KEY.equals(pubKeyAsHex)) - log.info("We got the DEV_PRIVILEGE_PUB_KEY in our list of publicKeys. RegistrationPubKey={}, nodeAddress={}", - Utilities.bytesAsHexString(e.getRegistrationPubKey()), - e.getNodeAddress().getFullAddress()); - else - log.warn("We got an arbitrator which is not in our list of publicKeys. RegistrationPubKey={}, nodeAddress={}", - Utilities.bytesAsHexString(e.getRegistrationPubKey()), - e.getNodeAddress().getFullAddress()); - } - final boolean isSigValid = verifySignature(e.getPubKeyRing().getSignaturePubKey(), - e.getRegistrationPubKey(), - e.getRegistrationSignature()); - if (!isSigValid) - log.warn("Sig check for arbitrator failed. Arbitrator=", e.toString()); - - return isInPublicKeyInList && isSigValid; - }) - .collect(Collectors.toMap(Arbitrator::getNodeAddress, Function.identity())); - - arbitratorsObservableMap.putAll(filtered); - arbitratorsObservableMap.values().stream() - .filter(persistedAcceptedArbitrators::contains) - .forEach(a -> { - user.addAcceptedArbitrator(a); - user.addAcceptedMediator(getMediator(a) - ); - }); - - // We keep the domain with storing the arbitrators in user as it might be still useful for mediators - arbitratorsObservableMap.values().forEach(a -> { - user.addAcceptedArbitrator(a); - user.addAcceptedMediator(getMediator(a) - ); - }); - - log.info("Available arbitrators: {}", arbitratorsObservableMap.keySet()); - } - - // TODO we mirror arbitrator data for mediator as long we have not impl. it in the UI - @NotNull - public static Mediator getMediator(Arbitrator arbitrator) { - return new Mediator(arbitrator.getNodeAddress(), - arbitrator.getPubKeyRing(), - arbitrator.getLanguageCodes(), - arbitrator.getRegistrationDate(), - arbitrator.getRegistrationPubKey(), - arbitrator.getRegistrationSignature(), - arbitrator.getEmailAddress(), - null, - arbitrator.getExtraDataMap()); - } - - public void addArbitrator(Arbitrator arbitrator, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - user.setRegisteredArbitrator(arbitrator); - arbitratorsObservableMap.put(arbitrator.getNodeAddress(), arbitrator); - arbitratorService.addArbitrator(arbitrator, - () -> { - log.debug("Arbitrator successfully saved in P2P network"); - resultHandler.handleResult(); - - if (arbitratorsObservableMap.size() > 0) - UserThread.runAfter(this::updateArbitratorMap, 100, TimeUnit.MILLISECONDS); - }, - errorMessageHandler::handleErrorMessage); - } - - public void removeArbitrator(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - Arbitrator registeredArbitrator = user.getRegisteredArbitrator(); - if (registeredArbitrator != null) { - user.setRegisteredArbitrator(null); - arbitratorsObservableMap.remove(registeredArbitrator.getNodeAddress()); - arbitratorService.removeArbitrator(registeredArbitrator, - () -> { - log.debug("Arbitrator successfully removed from P2P network"); - resultHandler.handleResult(); - }, - errorMessageHandler::handleErrorMessage); - } - } - - public ObservableMap getArbitratorsObservableMap() { - return arbitratorsObservableMap; - } - - // A private key is handed over to selected arbitrators for registration. - // An invited arbitrator will sign at registration his storageSignaturePubKey with that private key and attach the signature and pubKey to his data. - // Other users will check the signature with the list of public keys hardcoded in the app. - public String signStorageSignaturePubKey(ECKey key) { - String keyToSignAsHex = Utils.HEX.encode(keyRing.getPubKeyRing().getSignaturePubKey().getEncoded()); - return key.signMessage(keyToSignAsHex); - } - - @Nullable - public ECKey getRegistrationKey(String privKeyBigIntString) { - try { - return ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyBigIntString))); - } catch (Throwable t) { - return null; - } - } - - public boolean isPublicKeyInList(String pubKeyAsHex) { - return publicKeys.contains(pubKeyAsHex); - } - - public boolean isArbitratorAvailableForLanguage(String languageCode) { - return arbitratorsObservableMap.values().stream().anyMatch(arbitrator -> - arbitrator.getLanguageCodes().stream().anyMatch(lc -> lc.equals(languageCode))); - } - - public List getArbitratorLanguages(List nodeAddresses) { - return arbitratorsObservableMap.values().stream() - .filter(arbitrator -> nodeAddresses.stream().anyMatch(nodeAddress -> nodeAddress.equals(arbitrator.getNodeAddress()))) - .flatMap(arbitrator -> arbitrator.getLanguageCodes().stream()) - .distinct() - .collect(Collectors.toList()); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Private - /////////////////////////////////////////////////////////////////////////////////////////// - - private void republishArbitrator() { - Arbitrator registeredArbitrator = user.getRegisteredArbitrator(); - if (registeredArbitrator != null) { - addArbitrator(registeredArbitrator, - this::updateArbitratorMap, - errorMessage -> { - if (retryRepublishArbitratorTimer == null) - retryRepublishArbitratorTimer = UserThread.runPeriodically(() -> { - stopRetryRepublishArbitratorTimer(); - republishArbitrator(); - }, RETRY_REPUBLISH_SEC); - } - ); - } - } - - private boolean verifySignature(PublicKey storageSignaturePubKey, byte[] registrationPubKey, String signature) { - String keyToSignAsHex = Utils.HEX.encode(storageSignaturePubKey.getEncoded()); - try { - ECKey key = ECKey.fromPublicOnly(registrationPubKey); - key.verifyMessage(keyToSignAsHex, signature); - return true; - } catch (SignatureException e) { - log.warn("verifySignature failed"); - return false; - } - } - - - private void stopRetryRepublishArbitratorTimer() { - if (retryRepublishArbitratorTimer != null) { - retryRepublishArbitratorTimer.stop(); - retryRepublishArbitratorTimer = null; - } - } - - private void stopRepublishArbitratorTimer() { - if (republishArbitratorTimer != null) { - republishArbitratorTimer.stop(); - republishArbitratorTimer = null; - } - } - - public Optional getArbitratorByNodeAddress(NodeAddress nodeAddress) { - return arbitratorsObservableMap.containsKey(nodeAddress) ? - Optional.of(arbitratorsObservableMap.get(nodeAddress)) : - Optional.empty(); - } -} diff --git a/core/src/main/java/bisq/core/arbitration/ArbitratorService.java b/core/src/main/java/bisq/core/arbitration/ArbitratorService.java deleted file mode 100644 index 8e781cc5c1..0000000000 --- a/core/src/main/java/bisq/core/arbitration/ArbitratorService.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - -package bisq.core.arbitration; - -import bisq.core.app.BisqEnvironment; -import bisq.core.filter.FilterManager; - -import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.P2PService; -import bisq.network.p2p.storage.HashMapChangedListener; - -import bisq.common.app.DevEnv; -import bisq.common.handlers.ErrorMessageHandler; -import bisq.common.handlers.ResultHandler; -import bisq.common.util.Utilities; - -import javax.inject.Inject; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Used to store arbitrators profile and load map of arbitrators - */ -public class ArbitratorService { - private static final Logger log = LoggerFactory.getLogger(ArbitratorService.class); - - private final P2PService p2PService; - private final FilterManager filterManager; - - interface ArbitratorMapResultHandler { - void handleResult(Map arbitratorsMap); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor - /////////////////////////////////////////////////////////////////////////////////////////// - - @Inject - public ArbitratorService(P2PService p2PService, FilterManager filterManager) { - this.p2PService = p2PService; - this.filterManager = filterManager; - } - - public void addHashSetChangedListener(HashMapChangedListener hashMapChangedListener) { - p2PService.addHashSetChangedListener(hashMapChangedListener); - } - - public void addArbitrator(Arbitrator arbitrator, final ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - log.debug("addArbitrator arbitrator.hashCode() " + arbitrator.hashCode()); - if (!BisqEnvironment.getBaseCurrencyNetwork().isMainnet() || - !Utilities.encodeToHex(arbitrator.getRegistrationPubKey()).equals(DevEnv.DEV_PRIVILEGE_PUB_KEY)) { - boolean result = p2PService.addProtectedStorageEntry(arbitrator, true); - if (result) { - log.trace("Add arbitrator to network was successful. Arbitrator.hashCode() = " + arbitrator.hashCode()); - resultHandler.handleResult(); - } else { - errorMessageHandler.handleErrorMessage("Add arbitrator failed"); - } - } else { - log.error("Attempt to publish dev arbitrator on mainnet."); - errorMessageHandler.handleErrorMessage("Add arbitrator failed. Attempt to publish dev arbitrator on mainnet."); - } - } - - public void removeArbitrator(Arbitrator arbitrator, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - log.debug("removeArbitrator arbitrator.hashCode() " + arbitrator.hashCode()); - if (p2PService.removeData(arbitrator, true)) { - log.trace("Remove arbitrator from network was successful. Arbitrator.hashCode() = " + arbitrator.hashCode()); - resultHandler.handleResult(); - } else { - errorMessageHandler.handleErrorMessage("Remove arbitrator failed"); - } - } - - P2PService getP2PService() { - return p2PService; - } - - public Map getArbitrators() { - final List bannedArbitrators = filterManager.getFilter() != null ? filterManager.getFilter().getArbitrators() : null; - if (bannedArbitrators != null) - log.warn("bannedArbitrators=" + bannedArbitrators); - Set arbitratorSet = p2PService.getDataMap().values().stream() - .filter(data -> data.getProtectedStoragePayload() instanceof Arbitrator) - .map(data -> (Arbitrator) data.getProtectedStoragePayload()) - .filter(a -> bannedArbitrators == null || - !bannedArbitrators.contains(a.getNodeAddress().getFullAddress())) - .collect(Collectors.toSet()); - - Map map = new HashMap<>(); - for (Arbitrator arbitrator : arbitratorSet) { - NodeAddress arbitratorNodeAddress = arbitrator.getNodeAddress(); - if (!map.containsKey(arbitratorNodeAddress)) - map.put(arbitratorNodeAddress, arbitrator); - else - log.warn("arbitratorAddress already exist in arbitrator map. Seems an arbitrator object is already registered with the same address."); - } - return map; - } -} diff --git a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java b/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java deleted file mode 100644 index 1f5d721337..0000000000 --- a/core/src/main/java/bisq/core/arbitration/DisputeChatSession.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - -package bisq.core.arbitration; - -import bisq.core.arbitration.messages.DisputeCommunicationMessage; -import bisq.core.arbitration.messages.DisputeMessage; -import bisq.core.arbitration.messages.DisputeResultMessage; -import bisq.core.arbitration.messages.OpenNewDisputeMessage; -import bisq.core.arbitration.messages.PeerOpenedDisputeMessage; -import bisq.core.arbitration.messages.PeerPublishedDisputePayoutTxMessage; -import bisq.core.chat.ChatSession; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.crypto.PubKeyRing; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; - -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nullable; - -public class DisputeChatSession extends ChatSession { - private static final Logger log = LoggerFactory.getLogger(DisputeChatSession.class); - - @Nullable - private Dispute dispute; - private DisputeManager disputeManager; - - public DisputeChatSession(@Nullable Dispute dispute, - DisputeManager disputeManager) { - super(DisputeCommunicationMessage.Type.ARBITRATION); - this.dispute = dispute; - this.disputeManager = disputeManager; - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Dependent on selected dispute - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public boolean isClient() { - return dispute != null && disputeManager.isTrader(dispute); - } - - @Override - public String getTradeId() { - return dispute != null ? dispute.getTradeId() : ""; - } - - @Override - public PubKeyRing getClientPubKeyRing() { - // Get pubkeyring of trader. Arbitrator is considered server for the chat session - return dispute != null ? dispute.getTraderPubKeyRing() : null; - } - - @Override - public void addDisputeCommunicationMessage(DisputeCommunicationMessage message) { - if (dispute != null && (isClient() || (!isClient() && !message.isSystemMessage()))) - dispute.addDisputeCommunicationMessage(message); - } - - @Override - public void persist() { - disputeManager.getDisputes().persist(); - } - - @Override - public ObservableList getDisputeCommunicationMessages() { - return dispute != null ? dispute.getDisputeCommunicationMessages() : FXCollections.observableArrayList(); - } - - @Override - public boolean chatIsOpen() { - return dispute != null && !dispute.isClosed(); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Not dependent on selected dispute - /////////////////////////////////////////////////////////////////////////////////////////// - - @Nullable - @Override - public NodeAddress getPeerNodeAddress(DisputeCommunicationMessage message) { - Optional disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId()); - if (!disputeOptional.isPresent()) { - log.warn("Could not find dispute for tradeId = {} traderId = {}", - message.getTradeId(), message.getTraderId()); - return null; - } - return disputeManager.getNodeAddressPubKeyRingTuple(disputeOptional.get()).first; - } - - @Nullable - @Override - public PubKeyRing getPeerPubKeyRing(DisputeCommunicationMessage message) { - Optional disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId()); - if (!disputeOptional.isPresent()) { - log.warn("Could not find dispute for tradeId = {} traderId = {}", - message.getTradeId(), message.getTraderId()); - return null; - } - - return disputeManager.getNodeAddressPubKeyRingTuple(disputeOptional.get()).second; - } - - @Override - public void dispatchMessage(DisputeMessage message) { - log.info("Received {} with tradeId {} and uid {}", - message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); - - if (message instanceof OpenNewDisputeMessage) { - disputeManager.onOpenNewDisputeMessage((OpenNewDisputeMessage) message); - } else if (message instanceof PeerOpenedDisputeMessage) { - disputeManager.onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); - } else if (message instanceof DisputeCommunicationMessage) { - if (((DisputeCommunicationMessage) message).getType() != DisputeCommunicationMessage.Type.ARBITRATION) { - log.debug("Ignore non dispute type communication message"); - return; - } - disputeManager.getChatManager().onDisputeDirectMessage((DisputeCommunicationMessage) message); - } else if (message instanceof DisputeResultMessage) { - disputeManager.onDisputeResultMessage((DisputeResultMessage) message); - } else if (message instanceof PeerPublishedDisputePayoutTxMessage) { - disputeManager.onDisputedPayoutTxMessage((PeerPublishedDisputePayoutTxMessage) message); - } else { - log.warn("Unsupported message at dispatchMessage.\nmessage=" + message); - } - } - - @Override - public List getChatMessages() { - return disputeManager.getDisputes().getList().stream() - .flatMap(dispute -> dispute.getDisputeCommunicationMessages().stream()) - .collect(Collectors.toList()); - } - - @Override - public boolean channelOpen(DisputeCommunicationMessage message) { - return disputeManager.findDispute(message.getTradeId(), message.getTraderId()).isPresent(); - } - - @Override - public void storeDisputeCommunicationMessage(DisputeCommunicationMessage message) { - Optional disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId()); - if (disputeOptional.isPresent()) { - if (disputeOptional.get().getDisputeCommunicationMessages().stream().noneMatch(m -> m.getUid().equals(message.getUid()))) { - disputeOptional.get().addDisputeCommunicationMessage(message); - } else { - log.warn("We got a disputeCommunicationMessage what we have already stored. UId = {} TradeId = {}", - message.getUid(), message.getTradeId()); - } - } - } -} diff --git a/core/src/main/java/bisq/core/arbitration/DisputeManager.java b/core/src/main/java/bisq/core/arbitration/DisputeManager.java deleted file mode 100644 index ddb7f1cb7c..0000000000 --- a/core/src/main/java/bisq/core/arbitration/DisputeManager.java +++ /dev/null @@ -1,929 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - -package bisq.core.arbitration; - -import bisq.core.arbitration.messages.DisputeCommunicationMessage; -import bisq.core.arbitration.messages.DisputeResultMessage; -import bisq.core.arbitration.messages.OpenNewDisputeMessage; -import bisq.core.arbitration.messages.PeerOpenedDisputeMessage; -import bisq.core.arbitration.messages.PeerPublishedDisputePayoutTxMessage; -import bisq.core.btc.exceptions.TransactionVerificationException; -import bisq.core.btc.exceptions.TxBroadcastException; -import bisq.core.btc.exceptions.WalletException; -import bisq.core.btc.setup.WalletsSetup; -import bisq.core.btc.wallet.BtcWalletService; -import bisq.core.btc.wallet.TradeWalletService; -import bisq.core.btc.wallet.TxBroadcaster; -import bisq.core.chat.ChatManager; -import bisq.core.locale.Res; -import bisq.core.offer.OpenOffer; -import bisq.core.offer.OpenOfferManager; -import bisq.core.trade.Contract; -import bisq.core.trade.Tradable; -import bisq.core.trade.Trade; -import bisq.core.trade.TradeManager; -import bisq.core.trade.closed.ClosedTradableManager; - -import bisq.network.p2p.BootstrapListener; -import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.P2PService; -import bisq.network.p2p.SendMailboxMessageListener; - -import bisq.common.Timer; -import bisq.common.UserThread; -import bisq.common.app.Version; -import bisq.common.crypto.KeyRing; -import bisq.common.crypto.PubKeyRing; -import bisq.common.handlers.FaultHandler; -import bisq.common.handlers.ResultHandler; -import bisq.common.proto.persistable.PersistedDataHost; -import bisq.common.storage.Storage; -import bisq.common.util.Tuple2; - -import org.bitcoinj.core.AddressFormatException; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.crypto.DeterministicKey; - -import com.google.inject.Inject; - -import org.fxmisc.easybind.EasyBind; -import org.fxmisc.easybind.Subscription; - -import javafx.beans.property.IntegerProperty; -import javafx.beans.property.SimpleIntegerProperty; - -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import lombok.Getter; - -import javax.annotation.Nullable; - -public class DisputeManager implements PersistedDataHost { - private static final Logger log = LoggerFactory.getLogger(DisputeManager.class); - - private final TradeWalletService tradeWalletService; - private final BtcWalletService walletService; - private final WalletsSetup walletsSetup; - private final TradeManager tradeManager; - private final ClosedTradableManager closedTradableManager; - private final OpenOfferManager openOfferManager; - private final P2PService p2PService; - private final KeyRing keyRing; - private final Storage disputeStorage; - @Getter - private DisputeList disputes; - private final String disputeInfo; - private final Map openDisputes; - private final Map closedDisputes; - private final Map delayMsgMap = new HashMap<>(); - - private final Map disputeIsClosedSubscriptionsMap = new HashMap<>(); - @Getter - private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty(); - @Getter - private final ChatManager chatManager; - - /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor - /////////////////////////////////////////////////////////////////////////////////////////// - - @Inject - public DisputeManager(P2PService p2PService, - TradeWalletService tradeWalletService, - BtcWalletService walletService, - WalletsSetup walletsSetup, - TradeManager tradeManager, - ClosedTradableManager closedTradableManager, - OpenOfferManager openOfferManager, - KeyRing keyRing, - Storage storage) { - this.p2PService = p2PService; - this.tradeWalletService = tradeWalletService; - this.walletService = walletService; - this.walletsSetup = walletsSetup; - this.tradeManager = tradeManager; - this.closedTradableManager = closedTradableManager; - this.openOfferManager = openOfferManager; - this.keyRing = keyRing; - - chatManager = new ChatManager(p2PService, walletsSetup); - chatManager.setChatSession(new DisputeChatSession(null, this)); - - disputeStorage = storage; - - openDisputes = new HashMap<>(); - closedDisputes = new HashMap<>(); - - disputeInfo = Res.get("support.initialInfo"); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void readPersisted() { - disputes = new DisputeList(disputeStorage); - disputes.readPersisted(); - disputes.stream().forEach(dispute -> dispute.setStorage(disputeStorage)); - } - - public void onAllServicesInitialized() { - chatManager.onAllServicesInitialized(); - p2PService.addP2PServiceListener(new BootstrapListener() { - @Override - public void onUpdatedDataReceived() { - chatManager.tryApplyMessages(); - } - }); - - walletsSetup.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> { - if (walletsSetup.isDownloadComplete()) - chatManager.tryApplyMessages(); - }); - - walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> { - if (walletsSetup.hasSufficientPeersForBroadcast()) - chatManager.tryApplyMessages(); - }); - - chatManager.tryApplyMessages(); - - cleanupDisputes(); - - disputes.getList().addListener((ListChangeListener) change -> { - change.next(); - onDisputesChangeListener(change.getAddedSubList(), change.getRemoved()); - }); - onDisputesChangeListener(disputes.getList(), null); - } - - private void onDisputesChangeListener(List addedList, - @Nullable List removedList) { - if (removedList != null) { - removedList.forEach(dispute -> { - String id = dispute.getId(); - if (disputeIsClosedSubscriptionsMap.containsKey(id)) { - disputeIsClosedSubscriptionsMap.get(id).unsubscribe(); - disputeIsClosedSubscriptionsMap.remove(id); - } - }); - } - addedList.forEach(dispute -> { - String id = dispute.getId(); - Subscription disputeStateSubscription = EasyBind.subscribe(dispute.isClosedProperty(), - isClosed -> { - // We get the event before the list gets updated, so we execute on next frame - UserThread.execute(() -> { - int openDisputes = disputes.getList().stream() - .filter(e -> !e.isClosed()) - .collect(Collectors.toList()).size(); - numOpenDisputes.set(openDisputes); - }); - }); - disputeIsClosedSubscriptionsMap.put(id, disputeStateSubscription); - }); - } - - public void cleanupDisputes() { - disputes.stream().forEach(dispute -> { - dispute.setStorage(disputeStorage); - if (dispute.isClosed()) - closedDisputes.put(dispute.getTradeId(), dispute); - else - openDisputes.put(dispute.getTradeId(), dispute); - }); - - // If we have duplicate disputes we close the second one (might happen if both traders opened a dispute and arbitrator - // was offline, so could not forward msg to other peer, then the arbitrator might have 4 disputes open for 1 trade) - openDisputes.forEach((key, openDispute) -> { - if (closedDisputes.containsKey(key)) { - final Dispute closedDispute = closedDisputes.get(key); - // We need to check if is from the same peer, we don't want to close the peers dispute - if (closedDispute.getTraderId() == openDispute.getTraderId()) { - openDispute.setIsClosed(true); - tradeManager.closeDisputedTrade(openDispute.getTradeId()); - } - } - }); - } - - public void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, ResultHandler resultHandler, FaultHandler faultHandler) { - if (!disputes.contains(dispute)) { - final Optional storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId()); - if (!storedDisputeOptional.isPresent() || reOpen) { - String sysMsg = dispute.isSupportTicket() ? - Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION) - : Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION); - - DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( - chatManager.getChatSession().getType(), - dispute.getTradeId(), - keyRing.getPubKeyRing().hashCode(), - false, - Res.get("support.systemMsg", sysMsg), - p2PService.getAddress() - ); - disputeCommunicationMessage.setSystemMessage(true); - dispute.addDisputeCommunicationMessage(disputeCommunicationMessage); - if (!reOpen) { - disputes.add(dispute); - } - - NodeAddress peersNodeAddress = dispute.getContract().getArbitratorNodeAddress(); - OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute, p2PService.getAddress(), - UUID.randomUUID().toString()); - log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "disputeCommunicationMessage.uid={}", - openNewDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), - disputeCommunicationMessage.getUid()); - p2PService.sendEncryptedMailboxMessage(peersNodeAddress, - dispute.getArbitratorPubKeyRing(), - openNewDisputeMessage, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "disputeCommunicationMessage.uid={}", - openNewDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), - disputeCommunicationMessage.getUid()); - - // We use the disputeCommunicationMessage wrapped inside the openNewDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - disputeCommunicationMessage.setArrived(true); - disputes.persist(); - resultHandler.handleResult(); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "disputeCommunicationMessage.uid={}", - openNewDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), - disputeCommunicationMessage.getUid()); - - // We use the disputeCommunicationMessage wrapped inside the openNewDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - disputeCommunicationMessage.setStoredInMailbox(true); - disputes.persist(); - resultHandler.handleResult(); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + - "disputeCommunicationMessage.uid={}, errorMessage={}", - openNewDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), - disputeCommunicationMessage.getUid(), errorMessage); - - // We use the disputeCommunicationMessage wrapped inside the openNewDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - disputeCommunicationMessage.setSendMessageError(errorMessage); - disputes.persist(); - faultHandler.handleFault("Sending dispute message failed: " + - errorMessage, new MessageDeliveryFailedException()); - } - } - ); - } else { - final String msg = "We got a dispute already open for that trade and trading peer.\n" + - "TradeId = " + dispute.getTradeId(); - log.warn(msg); - faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); - } - } else { - final String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); - log.warn(msg); - faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); - } - } - - // arbitrator sends that to trading peer when he received openDispute request - private String sendPeerOpenedDisputeMessage(Dispute disputeFromOpener, - Contract contractFromOpener, - PubKeyRing pubKeyRing) { - Dispute dispute = new Dispute( - disputeStorage, - disputeFromOpener.getTradeId(), - pubKeyRing.hashCode(), - !disputeFromOpener.isDisputeOpenerIsBuyer(), - !disputeFromOpener.isDisputeOpenerIsMaker(), - pubKeyRing, - disputeFromOpener.getTradeDate().getTime(), - contractFromOpener, - disputeFromOpener.getContractHash(), - disputeFromOpener.getDepositTxSerialized(), - disputeFromOpener.getPayoutTxSerialized(), - disputeFromOpener.getDepositTxId(), - disputeFromOpener.getPayoutTxId(), - disputeFromOpener.getContractAsJson(), - disputeFromOpener.getMakerContractSignature(), - disputeFromOpener.getTakerContractSignature(), - disputeFromOpener.getArbitratorPubKeyRing(), - disputeFromOpener.isSupportTicket() - ); - final Optional storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId()); - if (!storedDisputeOptional.isPresent()) { - String sysMsg = dispute.isSupportTicket() ? - Res.get("support.peerOpenedTicket", disputeInfo) - : Res.get("support.peerOpenedDispute", disputeInfo); - DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( - chatManager.getChatSession().getType(), - dispute.getTradeId(), - keyRing.getPubKeyRing().hashCode(), - false, - Res.get("support.systemMsg", sysMsg), - p2PService.getAddress() - ); - disputeCommunicationMessage.setSystemMessage(true); - dispute.addDisputeCommunicationMessage(disputeCommunicationMessage); - disputes.add(dispute); - - // we mirrored dispute already! - Contract contract = dispute.getContract(); - PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); - NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress(); - PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute, - p2PService.getAddress(), - UUID.randomUUID().toString()); - log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + - "disputeCommunicationMessage.uid={}", - peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), - disputeCommunicationMessage.getUid()); - p2PService.sendEncryptedMailboxMessage(peersNodeAddress, - peersPubKeyRing, - peerOpenedDisputeMessage, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + - "disputeCommunicationMessage.uid={}", - peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), - disputeCommunicationMessage.getUid()); - - // We use the disputeCommunicationMessage wrapped inside the peerOpenedDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - disputeCommunicationMessage.setArrived(true); - disputes.persist(); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + - "disputeCommunicationMessage.uid={}", - peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), - disputeCommunicationMessage.getUid()); - - // We use the disputeCommunicationMessage wrapped inside the peerOpenedDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - disputeCommunicationMessage.setStoredInMailbox(true); - disputes.persist(); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + - "disputeCommunicationMessage.uid={}, errorMessage={}", - peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, - peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), - disputeCommunicationMessage.getUid(), errorMessage); - - // We use the disputeCommunicationMessage wrapped inside the peerOpenedDisputeMessage for - // the state, as that is displayed to the user and we only persist that msg - disputeCommunicationMessage.setSendMessageError(errorMessage); - disputes.persist(); - } - } - ); - return null; - } else { - String msg = "We got a dispute already open for that trade and trading peer.\n" + - "TradeId = " + dispute.getTradeId(); - log.warn(msg); - return msg; - } - } - - // arbitrator send result to trader - public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String text) { - DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( - chatManager.getChatSession().getType(), - dispute.getTradeId(), - dispute.getTraderPubKeyRing().hashCode(), - false, - text, - p2PService.getAddress() - ); - - dispute.addDisputeCommunicationMessage(disputeCommunicationMessage); - disputeResult.setDisputeCommunicationMessage(disputeCommunicationMessage); - - NodeAddress peersNodeAddress; - Contract contract = dispute.getContract(); - if (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing())) - peersNodeAddress = contract.getBuyerNodeAddress(); - else - peersNodeAddress = contract.getSellerNodeAddress(); - DisputeResultMessage disputeResultMessage = new DisputeResultMessage(disputeResult, p2PService.getAddress(), - UUID.randomUUID().toString()); - log.info("Send {} to peer {}. tradeId={}, disputeResultMessage.uid={}, disputeCommunicationMessage.uid={}", - disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, disputeResultMessage.getTradeId(), - disputeResultMessage.getUid(), disputeCommunicationMessage.getUid()); - p2PService.sendEncryptedMailboxMessage(peersNodeAddress, - dispute.getTraderPubKeyRing(), - disputeResultMessage, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, disputeResultMessage.uid={}, " + - "disputeCommunicationMessage.uid={}", - disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, - disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), - disputeCommunicationMessage.getUid()); - - // We use the disputeCommunicationMessage wrapped inside the disputeResultMessage for - // the state, as that is displayed to the user and we only persist that msg - disputeCommunicationMessage.setArrived(true); - disputes.persist(); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, disputeResultMessage.uid={}, " + - "disputeCommunicationMessage.uid={}", - disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, - disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), - disputeCommunicationMessage.getUid()); - - // We use the disputeCommunicationMessage wrapped inside the disputeResultMessage for - // the state, as that is displayed to the user and we only persist that msg - disputeCommunicationMessage.setStoredInMailbox(true); - disputes.persist(); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, disputeResultMessage.uid={}, " + - "disputeCommunicationMessage.uid={}, errorMessage={}", - disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, - disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), - disputeCommunicationMessage.getUid(), errorMessage); - - // We use the disputeCommunicationMessage wrapped inside the disputeResultMessage for - // the state, as that is displayed to the user and we only persist that msg - disputeCommunicationMessage.setSendMessageError(errorMessage); - disputes.persist(); - } - } - ); - } - - // winner (or buyer in case of 50/50) sends tx to other peer - private void sendPeerPublishedPayoutTxMessage(Transaction transaction, Dispute dispute, Contract contract) { - PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); - NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress(); - log.trace("sendPeerPublishedPayoutTxMessage to peerAddress " + peersNodeAddress); - final PeerPublishedDisputePayoutTxMessage message = new PeerPublishedDisputePayoutTxMessage(transaction.bitcoinSerialize(), - dispute.getTradeId(), - p2PService.getAddress(), - UUID.randomUUID().toString()); - log.info("Send {} to peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - p2PService.sendEncryptedMailboxMessage(peersNodeAddress, - peersPubKeyRing, - message, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); - } - } - ); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Incoming message - /////////////////////////////////////////////////////////////////////////////////////////// - - // arbitrator receives that from trader who opens dispute - public void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) { - String errorMessage; - Dispute dispute = openNewDisputeMessage.getDispute(); - Contract contractFromOpener = dispute.getContract(); - PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getSellerPubKeyRing() : contractFromOpener.getBuyerPubKeyRing(); - if (isArbitrator(dispute)) { - if (!disputes.contains(dispute)) { - final Optional storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId()); - if (!storedDisputeOptional.isPresent()) { - dispute.setStorage(disputeStorage); - disputes.add(dispute); - errorMessage = sendPeerOpenedDisputeMessage(dispute, contractFromOpener, peersPubKeyRing); - } else { - errorMessage = "We got a dispute already open for that trade and trading peer.\n" + - "TradeId = " + dispute.getTradeId(); - log.warn(errorMessage); - } - } else { - errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); - log.warn(errorMessage); - } - } else { - errorMessage = "Trader received openNewDisputeMessage. That must never happen."; - log.error(errorMessage); - } - - // We use the DisputeCommunicationMessage not the openNewDisputeMessage for the ACK - ObservableList messages = openNewDisputeMessage.getDispute().getDisputeCommunicationMessages(); - if (!messages.isEmpty()) { - DisputeCommunicationMessage msg = messages.get(0); - PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getBuyerPubKeyRing() : contractFromOpener.getSellerPubKeyRing(); - chatManager.sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage); - } - } - - // not dispute requester receives that from arbitrator - public void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) { - String errorMessage; - Dispute dispute = peerOpenedDisputeMessage.getDispute(); - if (!isArbitrator(dispute)) { - if (!disputes.contains(dispute)) { - final Optional storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId()); - if (!storedDisputeOptional.isPresent()) { - dispute.setStorage(disputeStorage); - disputes.add(dispute); - Optional tradeOptional = tradeManager.getTradeById(dispute.getTradeId()); - tradeOptional.ifPresent(trade -> trade.setDisputeState(Trade.DisputeState.DISPUTE_STARTED_BY_PEER)); - errorMessage = null; - } else { - errorMessage = "We got a dispute already open for that trade and trading peer.\n" + - "TradeId = " + dispute.getTradeId(); - log.warn(errorMessage); - } - } else { - errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); - log.warn(errorMessage); - } - } else { - errorMessage = "Arbitrator received peerOpenedDisputeMessage. That must never happen."; - log.error(errorMessage); - } - - // We use the DisputeCommunicationMessage not the peerOpenedDisputeMessage for the ACK - ObservableList messages = peerOpenedDisputeMessage.getDispute().getDisputeCommunicationMessages(); - if (!messages.isEmpty()) { - DisputeCommunicationMessage msg = messages.get(0); - chatManager.sendAckMessage(msg, dispute.getArbitratorPubKeyRing(), errorMessage == null, errorMessage); - } - - chatManager.sendAckMessage(peerOpenedDisputeMessage, dispute.getArbitratorPubKeyRing(), errorMessage == null, errorMessage); - } - - // We get that message at both peers. The dispute object is in context of the trader - public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { - String errorMessage = null; - boolean success = false; - PubKeyRing arbitratorsPubKeyRing = null; - DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); - - if (isArbitrator(disputeResult)) { - log.error("Arbitrator received disputeResultMessage. That must never happen."); - return; - } - - final String tradeId = disputeResult.getTradeId(); - Optional disputeOptional = findDispute(tradeId, disputeResult.getTraderId()); - final String uid = disputeResultMessage.getUid(); - if (!disputeOptional.isPresent()) { - log.debug("We got a dispute result msg but we don't have a matching dispute. " + - "That might happen when we get the disputeResultMessage before the dispute was created. " + - "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); - if (!delayMsgMap.containsKey(uid)) { - // We delay2 sec. to be sure the comm. msg gets added first - Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2); - delayMsgMap.put(uid, timer); - } else { - log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + - "That should never happen. TradeId = " + tradeId); - } - return; - } - Dispute dispute = disputeOptional.get(); - try { - cleanupRetryMap(uid); - arbitratorsPubKeyRing = dispute.getArbitratorPubKeyRing(); - DisputeCommunicationMessage disputeCommunicationMessage = disputeResult.getDisputeCommunicationMessage(); - if (!dispute.getDisputeCommunicationMessages().contains(disputeCommunicationMessage)) - dispute.addDisputeCommunicationMessage(disputeCommunicationMessage); - else if (disputeCommunicationMessage != null) - log.warn("We got a dispute mail msg what we have already stored. TradeId = " + disputeCommunicationMessage.getTradeId()); - - dispute.setIsClosed(true); - - if (dispute.disputeResultProperty().get() != null) - log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " + - "again because the first close did not succeed. TradeId = " + tradeId); - - dispute.setDisputeResult(disputeResult); - - // We need to avoid publishing the tx from both traders as it would create problems with zero confirmation withdrawals - // There would be different transactions if both sign and publish (signers: once buyer+arb, once seller+arb) - // The tx publisher is the winner or in case both get 50% the buyer, as the buyer has more inventive to publish the tx as he receives - // more BTC as he has deposited - final Contract contract = dispute.getContract(); - - boolean isBuyer = keyRing.getPubKeyRing().equals(contract.getBuyerPubKeyRing()); - DisputeResult.Winner publisher = disputeResult.getWinner(); - - // Sometimes the user who receives the trade amount is never online, so we might want to - // let the loser publish the tx. When the winner comes online he gets his funds as it was published by the other peer. - // Default isLoserPublisher is set to false - if (disputeResult.isLoserPublisher()) { - // we invert the logic - if (publisher == DisputeResult.Winner.BUYER) - publisher = DisputeResult.Winner.SELLER; - else if (publisher == DisputeResult.Winner.SELLER) - publisher = DisputeResult.Winner.BUYER; - } - - if ((isBuyer && publisher == DisputeResult.Winner.BUYER) - || (!isBuyer && publisher == DisputeResult.Winner.SELLER)) { - - final Optional tradeOptional = tradeManager.getTradeById(tradeId); - Transaction payoutTx = null; - if (tradeOptional.isPresent()) { - payoutTx = tradeOptional.get().getPayoutTx(); - } else { - final Optional tradableOptional = closedTradableManager.getTradableById(tradeId); - if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) { - payoutTx = ((Trade) tradableOptional.get()).getPayoutTx(); - } - } - - if (payoutTx == null) { - if (dispute.getDepositTxSerialized() != null) { - byte[] multiSigPubKey = isBuyer ? contract.getBuyerMultiSigPubKey() : contract.getSellerMultiSigPubKey(); - DeterministicKey multiSigKeyPair = walletService.getMultiSigKeyPair(dispute.getTradeId(), multiSigPubKey); - Transaction signedDisputedPayoutTx = tradeWalletService.traderSignAndFinalizeDisputedPayoutTx( - dispute.getDepositTxSerialized(), - disputeResult.getArbitratorSignature(), - disputeResult.getBuyerPayoutAmount(), - disputeResult.getSellerPayoutAmount(), - contract.getBuyerPayoutAddressString(), - contract.getSellerPayoutAddressString(), - multiSigKeyPair, - contract.getBuyerMultiSigPubKey(), - contract.getSellerMultiSigPubKey(), - disputeResult.getArbitratorPubKey() - ); - Transaction committedDisputedPayoutTx = tradeWalletService.addTxToWallet(signedDisputedPayoutTx); - tradeWalletService.broadcastTx(committedDisputedPayoutTx, new TxBroadcaster.Callback() { - @Override - public void onSuccess(Transaction transaction) { - // after successful publish we send peer the tx - - dispute.setDisputePayoutTxId(transaction.getHashAsString()); - sendPeerPublishedPayoutTxMessage(transaction, dispute, contract); - - // set state after payout as we call swapTradeEntryToAvailableEntry - if (tradeManager.getTradeById(dispute.getTradeId()).isPresent()) - tradeManager.closeDisputedTrade(dispute.getTradeId()); - else { - Optional openOfferOptional = openOfferManager.getOpenOfferById(dispute.getTradeId()); - openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); - } - } - - @Override - public void onFailure(TxBroadcastException exception) { - log.error(exception.getMessage()); - } - }, 15); - - success = true; - } else { - errorMessage = "DepositTx is null. TradeId = " + tradeId; - log.warn(errorMessage); - success = false; - } - } else { - log.warn("We got already a payout tx. That might be the case if the other peer did not get the " + - "payout tx and opened a dispute. TradeId = " + tradeId); - dispute.setDisputePayoutTxId(payoutTx.getHashAsString()); - sendPeerPublishedPayoutTxMessage(payoutTx, dispute, contract); - - success = true; - } - - } else { - log.trace("We don't publish the tx as we are not the winning party."); - // Clean up tangling trades - if (dispute.disputeResultProperty().get() != null && - dispute.isClosed() && - tradeManager.getTradeById(dispute.getTradeId()).isPresent()) { - tradeManager.closeDisputedTrade(dispute.getTradeId()); - } - - success = true; - } - } catch (TransactionVerificationException e) { - errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString(); - log.error(errorMessage, e); - success = false; - - // We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used - // we get a TransactionVerificationException. No reason to keep that dispute open... - if (tradeManager.getTradeById(dispute.getTradeId()).isPresent()) - tradeManager.closeDisputedTrade(dispute.getTradeId()); - else { - Optional openOfferOptional = openOfferManager.getOpenOfferById(dispute.getTradeId()); - openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); - } - dispute.setIsClosed(true); - - throw new RuntimeException(errorMessage); - } catch (AddressFormatException | WalletException e) { - errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString(); - log.error(errorMessage, e); - success = false; - throw new RuntimeException(errorMessage); - } finally { - if (arbitratorsPubKeyRing != null) { - // We use the disputeCommunicationMessage as we only persist those not the disputeResultMessage. - // If we would use the disputeResultMessage we could not lookup for the msg when we receive the AckMessage. - DisputeCommunicationMessage disputeCommunicationMessage = disputeResultMessage.getDisputeResult().getDisputeCommunicationMessage(); - chatManager.sendAckMessage(disputeCommunicationMessage, arbitratorsPubKeyRing, success, errorMessage); - } - } - } - - // Losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer - public void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) { - final String uid = peerPublishedDisputePayoutTxMessage.getUid(); - final String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId(); - Optional disputeOptional = findOwnDispute(tradeId); - if (!disputeOptional.isPresent()) { - log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId); - if (!delayMsgMap.containsKey(uid)) { - // We delay 3 sec. to be sure the close msg gets added first - Timer timer = UserThread.runAfter(() -> onDisputedPayoutTxMessage(peerPublishedDisputePayoutTxMessage), 3); - delayMsgMap.put(uid, timer); - } else { - log.warn("We got a peerPublishedPayoutTxMessage after we already repeated to apply the message after a delay. " + - "That should never happen. TradeId = " + tradeId); - } - return; - } - - Dispute dispute = disputeOptional.get(); - final Contract contract = dispute.getContract(); - PubKeyRing ownPubKeyRing = keyRing.getPubKeyRing(); - boolean isBuyer = ownPubKeyRing.equals(contract.getBuyerPubKeyRing()); - PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); - - cleanupRetryMap(uid); - Transaction walletTx = tradeWalletService.addTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction()); - dispute.setDisputePayoutTxId(walletTx.getHashAsString()); - BtcWalletService.printTx("Disputed payoutTx received from peer", walletTx); - - // We can only send the ack msg if we have the peersPubKeyRing which requires the dispute - chatManager.sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Getters - /////////////////////////////////////////////////////////////////////////////////////////// - - public Storage getDisputeStorage() { - return disputeStorage; - } - - public ObservableList getDisputesAsObservableList() { - return disputes.getList(); - } - - public boolean isTrader(Dispute dispute) { - return keyRing.getPubKeyRing().equals(dispute.getTraderPubKeyRing()); - } - - private boolean isArbitrator(Dispute dispute) { - return keyRing.getPubKeyRing().equals(dispute.getArbitratorPubKeyRing()); - } - - private boolean isArbitrator(DisputeResult disputeResult) { - return Arrays.equals(disputeResult.getArbitratorPubKey(), - walletService.getArbitratorAddressEntry().getPubKey()); - } - - public String getNrOfDisputes(boolean isBuyer, Contract contract) { - return String.valueOf(getDisputesAsObservableList().stream() - .filter(e -> { - Contract contract1 = e.getContract(); - if (contract1 == null) - return false; - - if (isBuyer) { - NodeAddress buyerNodeAddress = contract1.getBuyerNodeAddress(); - return buyerNodeAddress != null && buyerNodeAddress.equals(contract.getBuyerNodeAddress()); - } else { - NodeAddress sellerNodeAddress = contract1.getSellerNodeAddress(); - return sellerNodeAddress != null && sellerNodeAddress.equals(contract.getSellerNodeAddress()); - } - }) - .collect(Collectors.toSet()).size()); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Utils - /////////////////////////////////////////////////////////////////////////////////////////// - - - public Tuple2 getNodeAddressPubKeyRingTuple(Dispute dispute) { - PubKeyRing receiverPubKeyRing = null; - NodeAddress peerNodeAddress = null; - if (isTrader(dispute)) { - receiverPubKeyRing = dispute.getArbitratorPubKeyRing(); - peerNodeAddress = dispute.getContract().getArbitratorNodeAddress(); - } else if (isArbitrator(dispute)) { - receiverPubKeyRing = dispute.getTraderPubKeyRing(); - Contract contract = dispute.getContract(); - if (contract.getBuyerPubKeyRing().equals(receiverPubKeyRing)) - peerNodeAddress = contract.getBuyerNodeAddress(); - else - peerNodeAddress = contract.getSellerNodeAddress(); - } else { - log.error("That must not happen. Trader cannot communicate to other trader."); - } - return new Tuple2<>(peerNodeAddress, receiverPubKeyRing); - } - - public Optional findDispute(String tradeId, int traderId) { - return disputes.stream().filter(e -> e.getTradeId().equals(tradeId) && e.getTraderId() == traderId).findAny(); - } - - public Optional findOwnDispute(String tradeId) { - return getDisputeStream(tradeId).findAny(); - } - - private Stream getDisputeStream(String tradeId) { - return disputes.stream().filter(e -> e.getTradeId().equals(tradeId)); - } - - private void cleanupRetryMap(String uid) { - if (delayMsgMap.containsKey(uid)) { - Timer timer = delayMsgMap.remove(uid); - if (timer != null) - timer.stop(); - } - } - -} diff --git a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java index 3db2070e5d..dab65b733a 100644 --- a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java @@ -341,7 +341,10 @@ public class TradeWalletService { * @return A data container holding the inputs, the output value and address * @throws TransactionVerificationException */ - public InputsAndChangeOutput takerCreatesDepositsTxInputs(Transaction takeOfferFeeTx, Coin inputAmount, Coin txFee, Address takersAddress) throws + public InputsAndChangeOutput takerCreatesDepositsTxInputs(Transaction takeOfferFeeTx, + Coin inputAmount, + Coin txFee, + Address takersAddress) throws TransactionVerificationException { log.debug("takerCreatesDepositsTxInputs called"); log.debug("inputAmount {}", inputAmount.toFriendlyString()); @@ -793,7 +796,112 @@ public class TradeWalletService { /////////////////////////////////////////////////////////////////////////////////////////// - // Dispute + // Mediation + /////////////////////////////////////////////////////////////////////////////////////////// + + public byte[] signMediatedPayoutTx(Transaction depositTx, + Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + String buyerPayoutAddressString, + String sellerPayoutAddressString, + DeterministicKey myMultiSigKeyPair, + byte[] buyerPubKey, + byte[] sellerPubKey, + byte[] arbitratorPubKey) + throws AddressFormatException, TransactionVerificationException { + log.trace("signMediatedPayoutTx called"); + log.trace("depositTx {}", depositTx.toString()); + log.trace("buyerPayoutAmount {}", buyerPayoutAmount.toFriendlyString()); + log.trace("sellerPayoutAmount {}", sellerPayoutAmount.toFriendlyString()); + log.trace("buyerPayoutAddressString {}", buyerPayoutAddressString); + log.trace("sellerPayoutAddressString {}", sellerPayoutAddressString); + log.trace("multiSigKeyPair (not displayed for security reasons)"); + log.trace("buyerPubKey HEX=" + ECKey.fromPublicOnly(buyerPubKey).getPublicKeyAsHex()); + log.trace("sellerPubKey HEX=" + ECKey.fromPublicOnly(sellerPubKey).getPublicKeyAsHex()); + log.trace("arbitratorPubKey HEX=" + ECKey.fromPublicOnly(arbitratorPubKey).getPublicKeyAsHex()); + Transaction preparedPayoutTx = createPayoutTx(depositTx, + buyerPayoutAmount, + sellerPayoutAmount, + buyerPayoutAddressString, + sellerPayoutAddressString); + // MS redeemScript + Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); + // MS output from prev. tx is index 0 + Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); + checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null"); + if (myMultiSigKeyPair.isEncrypted()) + checkNotNull(aesKey); + + ECKey.ECDSASignature mySignature = myMultiSigKeyPair.sign(sigHash, aesKey).toCanonicalised(); + + WalletService.printTx("prepared mediated payoutTx for sig creation", preparedPayoutTx); + + WalletService.verifyTransaction(preparedPayoutTx); + + return mySignature.encodeToDER(); + } + + public Transaction finalizeMediatedPayoutTx(Transaction depositTx, + byte[] buyerSignature, + byte[] sellerSignature, + Coin buyerPayoutAmount, + Coin sellerPayoutAmount, + String buyerPayoutAddressString, + String sellerPayoutAddressString, + DeterministicKey multiSigKeyPair, + byte[] buyerPubKey, + byte[] sellerPubKey, + byte[] arbitratorPubKey) + throws AddressFormatException, TransactionVerificationException, WalletException { + log.trace("finalizeMediatedPayoutTx called"); + log.trace("depositTx {}", depositTx.toString()); + log.trace("buyerSignature r {}", ECKey.ECDSASignature.decodeFromDER(buyerSignature).r.toString()); + log.trace("buyerSignature s {}", ECKey.ECDSASignature.decodeFromDER(buyerSignature).s.toString()); + log.trace("sellerSignature r {}", ECKey.ECDSASignature.decodeFromDER(sellerSignature).r.toString()); + log.trace("sellerSignature s {}", ECKey.ECDSASignature.decodeFromDER(sellerSignature).s.toString()); + log.trace("buyerPayoutAmount {}", buyerPayoutAmount.toFriendlyString()); + log.trace("sellerPayoutAmount {}", sellerPayoutAmount.toFriendlyString()); + log.trace("buyerPayoutAddressString {}", buyerPayoutAddressString); + log.trace("sellerPayoutAddressString {}", sellerPayoutAddressString); + log.trace("multiSigKeyPair (not displayed for security reasons)"); + log.trace("buyerPubKey {}", ECKey.fromPublicOnly(buyerPubKey).toString()); + log.trace("sellerPubKey {}", ECKey.fromPublicOnly(sellerPubKey).toString()); + log.trace("arbitratorPubKey {}", ECKey.fromPublicOnly(arbitratorPubKey).toString()); + + Transaction payoutTx = createPayoutTx(depositTx, + buyerPayoutAmount, + sellerPayoutAmount, + buyerPayoutAddressString, + sellerPayoutAddressString); + // MS redeemScript + Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); + // MS output from prev. tx is index 0 + checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null"); + + TransactionSignature buyerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(buyerSignature), + Transaction.SigHash.ALL, false); + TransactionSignature sellerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(sellerSignature), + Transaction.SigHash.ALL, false); + + // Take care of order of signatures. Need to be reversed here. See comment below at getMultiSigRedeemScript (arbitrator, seller, buyer) + Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript); + + TransactionInput input = payoutTx.getInput(0); + input.setScriptSig(inputScript); + + WalletService.printTx("mediated payoutTx", payoutTx); + + WalletService.verifyTransaction(payoutTx); + WalletService.checkWalletConsistency(wallet); + WalletService.checkScriptSig(payoutTx, input, 0); + checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null"); + input.verify(input.getConnectedOutput()); + return payoutTx; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Arbitration /////////////////////////////////////////////////////////////////////////////////////////// /** @@ -1127,7 +1235,9 @@ public class TradeWalletService { } @NotNull - private TransactionInput getTransactionInput(Transaction depositTx, byte[] scriptProgram, RawTransactionInput rawTransactionInput) { + private TransactionInput getTransactionInput(Transaction depositTx, + byte[] scriptProgram, + RawTransactionInput rawTransactionInput) { return new TransactionInput(params, depositTx, scriptProgram, @@ -1191,7 +1301,10 @@ public class TradeWalletService { } } - private void addAvailableInputsAndChangeOutputs(Transaction transaction, Address address, Address changeAddress, Coin txFee) throws WalletException { + private void addAvailableInputsAndChangeOutputs(Transaction transaction, + Address address, + Address changeAddress, + Coin txFee) throws WalletException { SendRequest sendRequest = null; try { // Lets let the framework do the work to find the right inputs diff --git a/core/src/main/java/bisq/core/chat/ChatManager.java b/core/src/main/java/bisq/core/chat/ChatManager.java deleted file mode 100644 index 2d28df105d..0000000000 --- a/core/src/main/java/bisq/core/chat/ChatManager.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - -package bisq.core.chat; - -import bisq.core.arbitration.messages.DisputeCommunicationMessage; -import bisq.core.arbitration.messages.DisputeMessage; -import bisq.core.btc.setup.WalletsSetup; - -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.common.Timer; -import bisq.common.UserThread; -import bisq.common.crypto.PubKeyRing; -import bisq.common.proto.network.NetworkEnvelope; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.CopyOnWriteArraySet; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import lombok.Getter; -import lombok.Setter; - -import javax.annotation.Nullable; - -public class ChatManager { - private static final Logger log = LoggerFactory.getLogger(ChatManager.class); - - @Getter - private final P2PService p2PService; - private final WalletsSetup walletsSetup; - @Setter - @Getter - private ChatSession chatSession; - private final Map delayMsgMap = new HashMap<>(); - - private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); - private final CopyOnWriteArraySet decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>(); - private boolean allServicesInitialized; - - public ChatManager(P2PService p2PService, - WalletsSetup walletsSetup - ) { - this.p2PService = p2PService; - this.walletsSetup = walletsSetup; - - // We get first the message handler called then the onBootstrapped - p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> { - decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); - tryApplyMessages(); - }); - p2PService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> { - decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey); - tryApplyMessages(); - }); - } - - public void onAllServicesInitialized() { - allServicesInitialized = true; - } - - public void tryApplyMessages() { - if (isReady()) - applyMessages(); - } - - private boolean isReady() { - return allServicesInitialized && - p2PService.isBootstrapped() && - walletsSetup.isDownloadComplete() && - walletsSetup.hasSufficientPeersForBroadcast(); - } - - private void applyMessages() { - decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { - NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); - if (networkEnvelope instanceof DisputeMessage) { - chatSession.dispatchMessage((DisputeMessage) networkEnvelope); - } else if (networkEnvelope instanceof AckMessage) { - processAckMessage((AckMessage) networkEnvelope, null); - } - }); - decryptedDirectMessageWithPubKeys.clear(); - - decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { - NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); - log.debug("decryptedMessageWithPubKey.message " + networkEnvelope); - if (networkEnvelope instanceof DisputeMessage) { - chatSession.dispatchMessage((DisputeMessage) networkEnvelope); - p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey); - } else if (networkEnvelope instanceof AckMessage) { - processAckMessage((AckMessage) networkEnvelope, decryptedMessageWithPubKey); - } - }); - decryptedMailboxMessageWithPubKeys.clear(); - } - - public void onDisputeDirectMessage(DisputeCommunicationMessage disputeCommunicationMessage) { - final String tradeId = disputeCommunicationMessage.getTradeId(); - final String uid = disputeCommunicationMessage.getUid(); - boolean channelOpen = chatSession.channelOpen(disputeCommunicationMessage); - if (!channelOpen) { - log.debug("We got a disputeCommunicationMessage but we don't have a matching chat. TradeId = " + tradeId); - if (!delayMsgMap.containsKey(uid)) { - Timer timer = UserThread.runAfter(() -> onDisputeDirectMessage(disputeCommunicationMessage), 1); - delayMsgMap.put(uid, timer); - } else { - String msg = "We got a disputeCommunicationMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId; - log.warn(msg); - } - return; - } - - cleanupRetryMap(uid); - PubKeyRing receiverPubKeyRing = chatSession.getPeerPubKeyRing(disputeCommunicationMessage); - - chatSession.storeDisputeCommunicationMessage(disputeCommunicationMessage); - - // We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we - // cannot send it anyway) - if (receiverPubKeyRing != null) - sendAckMessage(disputeCommunicationMessage, receiverPubKeyRing, true, null); - } - - private void processAckMessage(AckMessage ackMessage, - @Nullable DecryptedMessageWithPubKey decryptedMessageWithPubKey) { - if (ackMessage.getSourceType() == AckMessageSourceType.DISPUTE_MESSAGE) { - if (ackMessage.isSuccess()) { - log.info("Received AckMessage for {} with tradeId {} and uid {}", - ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid()); - } else { - log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}", - ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage()); - } - - chatSession.getChatMessages() - .forEach(msg -> { - if (ackMessage.isSuccess()) - msg.setAcknowledged(true); - else - msg.setAckError(ackMessage.getErrorMessage()); - }); - chatSession.persist(); - - if (decryptedMessageWithPubKey != null) - p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey); - } - } - - public void sendAckMessage(DisputeMessage disputeMessage, PubKeyRing peersPubKeyRing, - boolean result, @Nullable String errorMessage) { - String tradeId = disputeMessage.getTradeId(); - String uid = disputeMessage.getUid(); - AckMessage ackMessage = new AckMessage(p2PService.getNetworkNode().getNodeAddress(), - AckMessageSourceType.DISPUTE_MESSAGE, - disputeMessage.getClass().getSimpleName(), - uid, - tradeId, - result, - errorMessage); - final NodeAddress peersNodeAddress = disputeMessage.getSenderNodeAddress(); - log.info("Send AckMessage for {} to peer {}. tradeId={}, uid={}", - ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); - p2PService.sendEncryptedMailboxMessage( - peersNodeAddress, - peersPubKeyRing, - ackMessage, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("AckMessage for {} arrived at peer {}. tradeId={}, uid={}", - ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); - } - - @Override - public void onStoredInMailbox() { - log.info("AckMessage for {} stored in mailbox for peer {}. tradeId={}, uid={}", - ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); - } - - @Override - public void onFault(String errorMessage) { - log.error("AckMessage for {} failed. Peer {}. tradeId={}, uid={}, errorMessage={}", - ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid, errorMessage); - } - } - ); - } - - private void cleanupRetryMap(String uid) { - if (delayMsgMap.containsKey(uid)) { - Timer timer = delayMsgMap.remove(uid); - if (timer != null) - timer.stop(); - } - } -} diff --git a/core/src/main/java/bisq/core/chat/ChatSession.java b/core/src/main/java/bisq/core/chat/ChatSession.java deleted file mode 100644 index 7727fc77e9..0000000000 --- a/core/src/main/java/bisq/core/chat/ChatSession.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - -package bisq.core.chat; - -import bisq.core.arbitration.messages.DisputeCommunicationMessage; -import bisq.core.arbitration.messages.DisputeMessage; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.crypto.PubKeyRing; - -import javafx.collections.ObservableList; - -import java.util.List; - -import lombok.Getter; - -public abstract class ChatSession { - @Getter - DisputeCommunicationMessage.Type type; - - public ChatSession(DisputeCommunicationMessage.Type type) { - this.type = type; - } - - abstract public boolean isClient(); - - abstract public String getTradeId(); - - abstract public PubKeyRing getClientPubKeyRing(); - - abstract public void addDisputeCommunicationMessage(DisputeCommunicationMessage message); - - abstract public void persist(); - - abstract public ObservableList getDisputeCommunicationMessages(); - - abstract public List getChatMessages(); - - abstract public boolean chatIsOpen(); - - abstract public NodeAddress getPeerNodeAddress(DisputeCommunicationMessage message); - - abstract public PubKeyRing getPeerPubKeyRing(DisputeCommunicationMessage message); - - abstract public void dispatchMessage(DisputeMessage message); - - abstract public boolean channelOpen(DisputeCommunicationMessage message); - - abstract public void storeDisputeCommunicationMessage(DisputeCommunicationMessage message); -} diff --git a/core/src/main/java/bisq/core/dao/governance/bond/BondRepository.java b/core/src/main/java/bisq/core/dao/governance/bond/BondRepository.java index 3b227b02e5..d3f69b79a0 100644 --- a/core/src/main/java/bisq/core/dao/governance/bond/BondRepository.java +++ b/core/src/main/java/bisq/core/dao/governance/bond/BondRepository.java @@ -198,11 +198,11 @@ public abstract class BondRepository impl // Protected /////////////////////////////////////////////////////////////////////////////////////////// - abstract protected T createBond(R bondedAsset); + protected abstract T createBond(R bondedAsset); - abstract protected void updateBond(T bond, R bondedAsset, TxOutput lockupTxOutput); + protected abstract void updateBond(T bond, R bondedAsset, TxOutput lockupTxOutput); - abstract protected Stream getBondedAssetStream(); + protected abstract Stream getBondedAssetStream(); protected void update() { log.debug("update"); diff --git a/core/src/main/java/bisq/core/dao/node/BsqNode.java b/core/src/main/java/bisq/core/dao/node/BsqNode.java index d0168beafe..042bd84ba4 100644 --- a/core/src/main/java/bisq/core/dao/node/BsqNode.java +++ b/core/src/main/java/bisq/core/dao/node/BsqNode.java @@ -202,7 +202,7 @@ public abstract class BsqNode implements DaoSetupService { return startBlockHeight; } - abstract protected void startParseBlocks(); + protected abstract void startParseBlocks(); protected void onParseBlockChainComplete() { log.info("onParseBlockChainComplete"); diff --git a/core/src/main/java/bisq/core/filter/Filter.java b/core/src/main/java/bisq/core/filter/Filter.java index 649f368108..8d92fa1783 100644 --- a/core/src/main/java/bisq/core/filter/Filter.java +++ b/core/src/main/java/bisq/core/filter/Filter.java @@ -94,6 +94,10 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { @Nullable private final String disableTradeBelowVersion; + // added in v1.1.6 + @Nullable + private final List mediators; + public Filter(List bannedOfferIds, List bannedNodeAddress, List bannedPaymentAccounts, @@ -106,7 +110,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { @Nullable List btcNodes, boolean disableDao, @Nullable String disableDaoBelowVersion, - @Nullable String disableTradeBelowVersion) { + @Nullable String disableTradeBelowVersion, + @Nullable List mediators) { this.bannedOfferIds = bannedOfferIds; this.bannedNodeAddress = bannedNodeAddress; this.bannedPaymentAccounts = bannedPaymentAccounts; @@ -120,6 +125,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { this.disableDao = disableDao; this.disableDaoBelowVersion = disableDaoBelowVersion; this.disableTradeBelowVersion = disableTradeBelowVersion; + this.mediators = mediators; } @@ -143,7 +149,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { @Nullable String disableTradeBelowVersion, String signatureAsBase64, byte[] ownerPubKeyBytes, - @Nullable Map extraDataMap) { + @Nullable Map extraDataMap, + @Nullable List mediators) { this(bannedOfferIds, bannedNodeAddress, bannedPaymentAccounts, @@ -156,7 +163,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { btcNodes, disableDao, disableDaoBelowVersion, - disableTradeBelowVersion); + disableTradeBelowVersion, + mediators); this.signatureAsBase64 = signatureAsBase64; this.ownerPubKeyBytes = ownerPubKeyBytes; this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); @@ -189,6 +197,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { Optional.ofNullable(disableDaoBelowVersion).ifPresent(builder::setDisableDaoBelowVersion); Optional.ofNullable(disableTradeBelowVersion).ifPresent(builder::setDisableTradeBelowVersion); Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData); + Optional.ofNullable(mediators).ifPresent(builder::addAllMediators); return protobuf.StoragePayload.newBuilder().setFilter(builder).build(); } @@ -211,7 +220,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { proto.getDisableTradeBelowVersion().isEmpty() ? null : proto.getDisableTradeBelowVersion(), proto.getSignatureAsBase64(), proto.getOwnerPubKeyBytes().toByteArray(), - CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); + CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(), + CollectionUtils.isEmpty(proto.getMediatorsList()) ? null : new ArrayList<>(proto.getMediatorsList())); } @@ -224,7 +234,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload { return TimeUnit.DAYS.toMillis(180); } - public void setSigAndPubKey(String signatureAsBase64, PublicKey ownerPubKey) { + void setSigAndPubKey(String signatureAsBase64, PublicKey ownerPubKey) { this.signatureAsBase64 = signatureAsBase64; this.ownerPubKey = ownerPubKey; diff --git a/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java b/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java index fc9f21e0aa..6559d27c92 100644 --- a/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java +++ b/core/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java @@ -17,13 +17,13 @@ package bisq.core.notifications.alerts; -import bisq.core.arbitration.Dispute; -import bisq.core.arbitration.DisputeManager; -import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.locale.Res; import bisq.core.notifications.MobileMessage; import bisq.core.notifications.MobileMessageType; import bisq.core.notifications.MobileNotificationService; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.messages.ChatMessage; import bisq.network.p2p.P2PService; @@ -43,18 +43,20 @@ public class DisputeMsgEvents { private final MobileNotificationService mobileNotificationService; @Inject - public DisputeMsgEvents(DisputeManager disputeManager, P2PService p2PService, MobileNotificationService mobileNotificationService) { + public DisputeMsgEvents(ArbitrationManager arbitrationManager, + P2PService p2PService, + MobileNotificationService mobileNotificationService) { this.p2PService = p2PService; this.mobileNotificationService = mobileNotificationService; // We need to handle it here in the constructor otherwise we get repeated the messages sent. - disputeManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { + arbitrationManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { c.next(); if (c.wasAdded()) { c.getAddedSubList().forEach(this::setDisputeListener); } }); - disputeManager.getDisputesAsObservableList().forEach(this::setDisputeListener); + arbitrationManager.getDisputesAsObservableList().forEach(this::setDisputeListener); } // We ignore that onAllServicesInitialized here @@ -64,22 +66,22 @@ public class DisputeMsgEvents { private void setDisputeListener(Dispute dispute) { //TODO use weak ref or remove listener log.debug("We got a dispute added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId()); - dispute.getDisputeCommunicationMessages().addListener((ListChangeListener) c -> { - log.debug("We got a DisputeCommunicationMessage added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId()); + dispute.getChatMessages().addListener((ListChangeListener) c -> { + log.debug("We got a ChatMessage added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId()); c.next(); if (c.wasAdded()) { - c.getAddedSubList().forEach(this::setDisputeCommunicationMessage); + c.getAddedSubList().forEach(this::setChatMessage); } }); //TODO test - if (!dispute.getDisputeCommunicationMessages().isEmpty()) - setDisputeCommunicationMessage(dispute.getDisputeCommunicationMessages().get(0)); + if (!dispute.getChatMessages().isEmpty()) + setChatMessage(dispute.getChatMessages().get(0)); } - private void setDisputeCommunicationMessage(DisputeCommunicationMessage disputeMsg) { + private void setChatMessage(ChatMessage disputeMsg) { // TODO we need to prevent to send msg for old dispute messages again at restart - // Maybe we need a new property in DisputeCommunicationMessage + // Maybe we need a new property in ChatMessage // As key is not set in initial iterations it seems we don't need an extra handling. // the mailbox msg is set a bit later so that triggers a notification, but not the old messages. diff --git a/core/src/main/java/bisq/core/offer/AvailabilityResult.java b/core/src/main/java/bisq/core/offer/AvailabilityResult.java index 135afa9bf0..9589568703 100644 --- a/core/src/main/java/bisq/core/offer/AvailabilityResult.java +++ b/core/src/main/java/bisq/core/offer/AvailabilityResult.java @@ -25,5 +25,6 @@ public enum AvailabilityResult { MARKET_PRICE_NOT_AVAILABLE, NO_ARBITRATORS, NO_MEDIATORS, - USER_IGNORED + USER_IGNORED, + MISSING_MANDATORY_CAPABILITY } diff --git a/core/src/main/java/bisq/core/offer/Offer.java b/core/src/main/java/bisq/core/offer/Offer.java index d0c505edf8..d52bd9471e 100644 --- a/core/src/main/java/bisq/core/offer/Offer.java +++ b/core/src/main/java/bisq/core/offer/Offer.java @@ -72,7 +72,8 @@ public class Offer implements NetworkPayload, PersistablePayload { // Market price might be different at maker's and takers side so we need a bit of tolerance. // The tolerance will get smaller once we have multiple price feeds avoiding fast price fluctuations // from one provider. - final static double PRICE_TOLERANCE = 0.01; + private final static double PRICE_TOLERANCE = 0.01; + /////////////////////////////////////////////////////////////////////////////////////////// // Enums @@ -390,14 +391,6 @@ public class Offer implements NetworkPayload, PersistablePayload { return offerPayload.getId(); } - public List getArbitratorNodeAddresses() { - return offerPayload.getArbitratorNodeAddresses(); - } - - public List getMediatorNodeAddresses() { - return offerPayload.getMediatorNodeAddresses(); - } - @Nullable public List getAcceptedBankIds() { return offerPayload.getAcceptedBankIds(); diff --git a/core/src/main/java/bisq/core/offer/OfferBookService.java b/core/src/main/java/bisq/core/offer/OfferBookService.java index 71b9b9d6e3..d99955d532 100644 --- a/core/src/main/java/bisq/core/offer/OfferBookService.java +++ b/core/src/main/java/bisq/core/offer/OfferBookService.java @@ -28,6 +28,7 @@ import bisq.network.p2p.storage.HashMapChangedListener; import bisq.network.p2p.storage.payload.ProtectedStorageEntry; import bisq.common.UserThread; +import bisq.common.app.Capability; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; import bisq.common.storage.JsonFileManager; @@ -42,6 +43,7 @@ import java.io.File; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -91,8 +93,10 @@ public class OfferBookService { if (data.getProtectedStoragePayload() instanceof OfferPayload) { OfferPayload offerPayload = (OfferPayload) data.getProtectedStoragePayload(); Offer offer = new Offer(offerPayload); - offer.setPriceFeedService(priceFeedService); - listener.onAdded(offer); + if (showOffer(offer)) { + offer.setPriceFeedService(priceFeedService); + listener.onAdded(offer); + } } }); } @@ -131,6 +135,11 @@ public class OfferBookService { } } + private boolean showOffer(Offer offer) { + return !OfferRestrictions.requiresUpdate() || + OfferRestrictions.hasOfferMandatoryCapability(offer, Capability.MEDIATION); + } + /////////////////////////////////////////////////////////////////////////////////////////// // API @@ -150,7 +159,9 @@ public class OfferBookService { } } - public void refreshTTL(OfferPayload offerPayload, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + public void refreshTTL(OfferPayload offerPayload, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { if (filterManager.requireUpdateToNewVersionForTrading()) { errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading")); return; @@ -164,15 +175,21 @@ public class OfferBookService { } } - public void activateOffer(Offer offer, @Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { + public void activateOffer(Offer offer, + @Nullable ResultHandler resultHandler, + @Nullable ErrorMessageHandler errorMessageHandler) { addOffer(offer, resultHandler, errorMessageHandler); } - public void deactivateOffer(OfferPayload offerPayload, @Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { + public void deactivateOffer(OfferPayload offerPayload, + @Nullable ResultHandler resultHandler, + @Nullable ErrorMessageHandler errorMessageHandler) { removeOffer(offerPayload, resultHandler, errorMessageHandler); } - public void removeOffer(OfferPayload offerPayload, @Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { + public void removeOffer(OfferPayload offerPayload, + @Nullable ResultHandler resultHandler, + @Nullable ErrorMessageHandler errorMessageHandler) { if (p2PService.removeData(offerPayload, true)) { if (resultHandler != null) resultHandler.handleResult(); @@ -191,6 +208,7 @@ public class OfferBookService { offer.setPriceFeedService(priceFeedService); return offer; }) + .filter(this::showOffer) .collect(Collectors.toList()); } @@ -236,7 +254,7 @@ public class OfferBookService { return null; } }) - .filter(e -> e != null) + .filter(Objects::nonNull) .collect(Collectors.toList()); jsonFileManager.writeToDisc(Utilities.objectToJson(offerForJsonList), "offers_statistics"); } diff --git a/core/src/main/java/bisq/core/offer/OfferPayload.java b/core/src/main/java/bisq/core/offer/OfferPayload.java index 0e131360cf..ea7ff0406d 100644 --- a/core/src/main/java/bisq/core/offer/OfferPayload.java +++ b/core/src/main/java/bisq/core/offer/OfferPayload.java @@ -31,6 +31,7 @@ import org.springframework.util.CollectionUtils; import java.security.PublicKey; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; @@ -71,11 +72,18 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay } // Keys for extra map + // Only set for fiat offers public static final String ACCOUNT_AGE_WITNESS_HASH = "accountAgeWitnessHash"; public static final String REFERRAL_ID = "referralId"; + // Only used in payment method F2F public static final String F2F_CITY = "f2fCity"; public static final String F2F_EXTRA_INFO = "f2fExtraInfo"; + // Comma separated list of ordinal of a bisq.common.app.Capability. E.g. ordinal of + // Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker + // of the offer supports both capabilities we add "11, 12" to capabilities. + public static final String CAPABILITIES = "capabilities"; + /////////////////////////////////////////////////////////////////////////////////////////// // Instance fields @@ -105,7 +113,11 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay private final String baseCurrencyCode; private final String counterCurrencyCode; + @Deprecated + // Not used anymore but we cannot set it Nullable or remove it to not break backward compatibility (diff. hash) private final List arbitratorNodeAddresses; + @Deprecated + // Not used anymore but we cannot set it Nullable or remove it to not break backward compatibility (diff. hash) private final List mediatorNodeAddresses; private final String paymentMethodId; private final String makerPaymentAccountId; @@ -298,9 +310,9 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay public static OfferPayload fromProto(protobuf.OfferPayload proto) { checkArgument(!proto.getOfferFeePaymentTxId().isEmpty(), "OfferFeePaymentTxId must be set in PB.OfferPayload"); List acceptedBankIds = proto.getAcceptedBankIdsList().isEmpty() ? - null : proto.getAcceptedBankIdsList().stream().collect(Collectors.toList()); + null : new ArrayList<>(proto.getAcceptedBankIdsList()); List acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ? - null : proto.getAcceptedCountryCodesList().stream().collect(Collectors.toList()); + null : new ArrayList<>(proto.getAcceptedCountryCodesList()); String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge()); Map extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(); @@ -388,8 +400,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay ",\n minAmount=" + minAmount + ",\n baseCurrencyCode='" + baseCurrencyCode + '\'' + ",\n counterCurrencyCode='" + counterCurrencyCode + '\'' + - ",\n arbitratorNodeAddresses=" + arbitratorNodeAddresses + - ",\n mediatorNodeAddresses=" + mediatorNodeAddresses + ",\n paymentMethodId='" + paymentMethodId + '\'' + ",\n makerPaymentAccountId='" + makerPaymentAccountId + '\'' + ",\n offerFeePaymentTxId='" + offerFeePaymentTxId + '\'' + diff --git a/core/src/main/java/bisq/core/offer/OfferRestrictions.java b/core/src/main/java/bisq/core/offer/OfferRestrictions.java index d91f6d414e..f51466b3c2 100644 --- a/core/src/main/java/bisq/core/offer/OfferRestrictions.java +++ b/core/src/main/java/bisq/core/offer/OfferRestrictions.java @@ -20,9 +20,25 @@ package bisq.core.offer; import bisq.core.payment.payload.PaymentMethod; import bisq.core.trade.Trade; +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; +import bisq.common.util.Utilities; + import org.bitcoinj.core.Coin; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Map; + public class OfferRestrictions { + // The date when traders who have not updated cannot take offers from updated clients and their offers become + // invisible for updated clients. + private static final Date REQUIRE_UPDATE_DATE = Utilities.getUTCDate(2019, GregorianCalendar.SEPTEMBER, 1); + + static boolean requiresUpdate() { + return new Date().after(REQUIRE_UPDATE_DATE); + } + public static Coin TOLERATED_SMALL_TRADE_AMOUNT = Coin.parseCoin("0.01"); public static boolean isOfferRisky(Offer offer) { @@ -53,7 +69,17 @@ public class OfferRestrictions { return isAmountRisky(offer.getMinAmount()); } - public static boolean isAmountRisky(Coin amount) { + private static boolean isAmountRisky(Coin amount) { return amount.isGreaterThan(TOLERATED_SMALL_TRADE_AMOUNT); } + + static boolean hasOfferMandatoryCapability(Offer offer, Capability mandatoryCapability) { + Map extraDataMap = offer.getOfferPayload().getExtraDataMap(); + if (extraDataMap != null && extraDataMap.containsKey(OfferPayload.CAPABILITIES)) { + String commaSeparatedOrdinals = extraDataMap.get(OfferPayload.CAPABILITIES); + Capabilities capabilities = Capabilities.fromStringList(commaSeparatedOrdinals); + return Capabilities.hasMandatoryCapability(capabilities, mandatoryCapability); + } + return false; + } } diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index 1e5f9b491f..c987bd2e18 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -38,6 +38,7 @@ import bisq.core.util.CoinUtil; import bisq.network.p2p.P2PService; +import bisq.common.app.Capabilities; import bisq.common.util.MathUtils; import org.bitcoinj.core.Coin; @@ -116,7 +117,9 @@ public class OfferUtil { * @param amount * @return */ - public static boolean isCurrencyForMakerFeeBtc(Preferences preferences, BsqWalletService bsqWalletService, Coin amount) { + public static boolean isCurrencyForMakerFeeBtc(Preferences preferences, + BsqWalletService bsqWalletService, + Coin amount) { boolean payFeeInBtc = preferences.getPayFeeInBtc(); boolean bsqForFeeAvailable = isBsqForMakerFeeAvailable(bsqWalletService, amount); return payFeeInBtc || !bsqForFeeAvailable; @@ -152,7 +155,9 @@ public class OfferUtil { } } - public static boolean isCurrencyForTakerFeeBtc(Preferences preferences, BsqWalletService bsqWalletService, Coin amount) { + public static boolean isCurrencyForTakerFeeBtc(Preferences preferences, + BsqWalletService bsqWalletService, + Coin amount) { boolean payFeeInBtc = preferences.getPayFeeInBtc(); boolean bsqForFeeAvailable = isBsqForTakerFeeAvailable(bsqWalletService, amount); return payFeeInBtc || !bsqForFeeAvailable; @@ -317,7 +322,9 @@ public class OfferUtil { } } - public static String getFeeWithFiatAmount(Coin makerFeeAsCoin, Optional optionalFeeInFiat, BSFormatter formatter) { + public static String getFeeWithFiatAmount(Coin makerFeeAsCoin, + Optional optionalFeeInFiat, + BSFormatter formatter) { String fee = makerFeeAsCoin != null ? formatter.formatCoinWithCode(makerFeeAsCoin) : Res.get("shared.na"); String feeInFiatAsString; if (optionalFeeInFiat != null && optionalFeeInFiat.isPresent()) { @@ -333,27 +340,24 @@ public class OfferUtil { ReferralIdService referralIdService, PaymentAccount paymentAccount, String currencyCode) { - Map extraDataMap = null; + Map extraDataMap = new HashMap<>(); if (CurrencyUtil.isFiatCurrency(currencyCode)) { - extraDataMap = new HashMap<>(); - final String myWitnessHashAsHex = accountAgeWitnessService.getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload()); + String myWitnessHashAsHex = accountAgeWitnessService.getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload()); extraDataMap.put(OfferPayload.ACCOUNT_AGE_WITNESS_HASH, myWitnessHashAsHex); } if (referralIdService.getOptionalReferralId().isPresent()) { - if (extraDataMap == null) - extraDataMap = new HashMap<>(); extraDataMap.put(OfferPayload.REFERRAL_ID, referralIdService.getOptionalReferralId().get()); } if (paymentAccount instanceof F2FAccount) { - if (extraDataMap == null) - extraDataMap = new HashMap<>(); extraDataMap.put(OfferPayload.F2F_CITY, ((F2FAccount) paymentAccount).getCity()); extraDataMap.put(OfferPayload.F2F_EXTRA_INFO, ((F2FAccount) paymentAccount).getExtraInfo()); } - return extraDataMap; + extraDataMap.put(OfferPayload.CAPABILITIES, Capabilities.app.toStringList()); + + return extraDataMap.isEmpty() ? null : extraDataMap; } public static void validateOfferData(FilterManager filterManager, diff --git a/core/src/main/java/bisq/core/offer/OpenOffer.java b/core/src/main/java/bisq/core/offer/OpenOffer.java index f12c898c14..2193b30752 100644 --- a/core/src/main/java/bisq/core/offer/OpenOffer.java +++ b/core/src/main/java/bisq/core/offer/OpenOffer.java @@ -60,6 +60,10 @@ public final class OpenOffer implements Tradable { @Setter @Nullable private NodeAddress arbitratorNodeAddress; + @Getter + @Setter + @Nullable + private NodeAddress mediatorNodeAddress; transient private Storage> storage; @@ -73,10 +77,14 @@ public final class OpenOffer implements Tradable { // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private OpenOffer(Offer offer, State state, @Nullable NodeAddress arbitratorNodeAddress) { + private OpenOffer(Offer offer, + State state, + @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress) { this.offer = offer; this.state = state; this.arbitratorNodeAddress = arbitratorNodeAddress; + this.mediatorNodeAddress = mediatorNodeAddress; if (this.state == State.RESERVED) setState(State.AVAILABLE); @@ -89,6 +97,7 @@ public final class OpenOffer implements Tradable { .setState(protobuf.OpenOffer.State.valueOf(state.name())); Optional.ofNullable(arbitratorNodeAddress).ifPresent(nodeAddress -> builder.setArbitratorNodeAddress(nodeAddress.toProtoMessage())); + Optional.ofNullable(mediatorNodeAddress).ifPresent(nodeAddress -> builder.setMediatorNodeAddress(nodeAddress.toProtoMessage())); return protobuf.Tradable.newBuilder().setOpenOffer(builder).build(); } @@ -96,7 +105,8 @@ public final class OpenOffer implements Tradable { public static Tradable fromProto(protobuf.OpenOffer proto) { return new OpenOffer(Offer.fromProto(proto.getOffer()), ProtoUtil.enumFromProto(OpenOffer.State.class, proto.getState().name()), - proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null); + proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, + proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null); } @@ -157,12 +167,15 @@ public final class OpenOffer implements Tradable { } } + @Override public String toString() { return "OpenOffer{" + - "\n\toffer=" + offer + - "\n\tstate=" + state + - '}'; + ",\n offer=" + offer + + ",\n state=" + state + + ",\n arbitratorNodeAddress=" + arbitratorNodeAddress + + ",\n mediatorNodeAddress=" + mediatorNodeAddress + + "\n}"; } } diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 382676ee57..6754512c6e 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -17,17 +17,18 @@ package bisq.core.offer; -import bisq.core.arbitration.ArbitratorManager; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.exceptions.TradePriceOutOfToleranceException; -import bisq.core.offer.availability.ArbitratorSelection; +import bisq.core.offer.availability.DisputeAgentSelection; import bisq.core.offer.messages.OfferAvailabilityRequest; import bisq.core.offer.messages.OfferAvailabilityResponse; import bisq.core.offer.placeoffer.PlaceOfferModel; import bisq.core.offer.placeoffer.PlaceOfferProtocol; import bisq.core.provider.price.PriceFeedService; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.trade.TradableList; import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.handlers.TransactionResultHandler; @@ -48,6 +49,8 @@ import bisq.network.p2p.peers.PeerManager; import bisq.common.Timer; import bisq.common.UserThread; +import bisq.common.app.Capabilities; +import bisq.common.app.Capability; import bisq.common.crypto.KeyRing; import bisq.common.crypto.PubKeyRing; import bisq.common.handlers.ErrorMessageHandler; @@ -100,6 +103,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private final Preferences preferences; private final TradeStatisticsManager tradeStatisticsManager; private final ArbitratorManager arbitratorManager; + private final MediatorManager mediatorManager; private final Storage> openOfferTradableListStorage; private final Map offersToBeEdited = new HashMap<>(); private boolean stopped; @@ -111,7 +115,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe // Constructor, Initialization /////////////////////////////////////////////////////////////////////////////////////////// - @SuppressWarnings("WeakerAccess") @Inject public OpenOfferManager(KeyRing keyRing, User user, @@ -125,6 +128,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe Preferences preferences, TradeStatisticsManager tradeStatisticsManager, ArbitratorManager arbitratorManager, + MediatorManager mediatorManager, Storage> storage) { this.keyRing = keyRing; this.user = user; @@ -138,6 +142,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe this.preferences = preferences; this.tradeStatisticsManager = tradeStatisticsManager; this.arbitratorManager = arbitratorManager; + this.mediatorManager = mediatorManager; openOfferTradableListStorage = storage; @@ -150,7 +155,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe @Override public void readPersisted() { openOffers = new TradableList<>(openOfferTradableListStorage, "OpenOffers"); - openOffers.forEach(e -> e.getOffer().setPriceFeedService(priceFeedService)); + openOffers.forEach(e -> { + Offer offer = e.getOffer(); + offer.setPriceFeedService(priceFeedService); + }); } public void onAllServicesInitialized() { @@ -182,8 +190,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe }); } - @SuppressWarnings("WeakerAccess") - public void shutDown() { + private void shutDown() { shutDown(null); } @@ -214,7 +221,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe removeOpenOffers(getObservableList(), completeHandler); } - public void removeOpenOffers(List openOffers, @Nullable Runnable completeHandler) { + private void removeOpenOffers(List openOffers, @Nullable Runnable completeHandler) { final int size = openOffers.size(); // Copy list as we remove in the loop List openOffersList = new ArrayList<>(openOffers); @@ -260,6 +267,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private void onBootstrapComplete() { stopped = false; + maybeUpdatePersistedOffers(); + // Republish means we send the complete offer object republishOffers(); startPeriodicRepublishOffersTimer(); @@ -345,7 +354,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe log.debug("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call."); } }, - errorMessageHandler::handleErrorMessage + errorMessageHandler ); placeOfferProtocol.placeOffer(); } @@ -365,7 +374,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - public void activateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + public void activateOpenOffer(OpenOffer openOffer, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { if (!offersToBeEdited.containsKey(openOffer.getId())) { Offer offer = openOffer.getOffer(); openOffer.setStorage(openOfferTradableListStorage); @@ -381,7 +392,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - public void deactivateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + public void deactivateOpenOffer(OpenOffer openOffer, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { Offer offer = openOffer.getOffer(); openOffer.setStorage(openOfferTradableListStorage); offerBookService.deactivateOffer(offer.getOfferPayload(), @@ -393,7 +406,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe errorMessageHandler); } - public void removeOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + public void removeOpenOffer(OpenOffer openOffer, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { if (!offersToBeEdited.containsKey(openOffer.getId())) { Offer offer = openOffer.getOffer(); if (openOffer.isDeactivated()) { @@ -409,7 +424,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - public void editOpenOfferStart(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + public void editOpenOfferStart(OpenOffer openOffer, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { if (offersToBeEdited.containsKey(openOffer.getId())) { log.warn("editOpenOfferStart called for an offer which is already in edit mode."); resultHandler.handleResult(); @@ -422,7 +439,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe resultHandler.handleResult(); } else { deactivateOpenOffer(openOffer, - () -> resultHandler.handleResult(), + resultHandler, errorMessage -> { offersToBeEdited.remove(openOffer.getId()); errorMessageHandler.handleErrorMessage(errorMessage); @@ -430,7 +447,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - public void editOpenOfferPublish(Offer editedOffer, OpenOffer.State originalState, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + public void editOpenOfferPublish(Offer editedOffer, + OpenOffer.State originalState, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { Optional openOfferOptional = getOpenOfferById(editedOffer.getId()); if (openOfferOptional.isPresent()) { @@ -458,13 +478,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - public void editOpenOfferCancel(OpenOffer openOffer, OpenOffer.State originalState, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + public void editOpenOfferCancel(OpenOffer openOffer, + OpenOffer.State originalState, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { if (offersToBeEdited.containsKey(openOffer.getId())) { offersToBeEdited.remove(openOffer.getId()); if (originalState.equals(OpenOffer.State.AVAILABLE)) { - activateOpenOffer(openOffer, () -> { - resultHandler.handleResult(); - }, errorMessageHandler); + activateOpenOffer(openOffer, resultHandler, errorMessageHandler); } else { resultHandler.handleResult(); } @@ -555,6 +576,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe Optional openOfferOptional = getOpenOfferById(request.offerId); AvailabilityResult availabilityResult; NodeAddress arbitratorNodeAddress = null; + NodeAddress mediatorNodeAddress = null; if (openOfferOptional.isPresent()) { OpenOffer openOffer = openOfferOptional.get(); if (openOffer.getState() == OpenOffer.State.AVAILABLE) { @@ -564,23 +586,35 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe List acceptedArbitrators = user.getAcceptedArbitratorAddresses(); if (acceptedArbitrators != null && !acceptedArbitrators.isEmpty()) { - arbitratorNodeAddress = ArbitratorSelection.getLeastUsedArbitrator(tradeStatisticsManager, arbitratorManager).getNodeAddress(); + arbitratorNodeAddress = DisputeAgentSelection.getLeastUsedArbitrator(tradeStatisticsManager, arbitratorManager).getNodeAddress(); openOffer.setArbitratorNodeAddress(arbitratorNodeAddress); - // Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference - // in trade price between the peers. Also here poor connectivity might cause market price API connection - // losses and therefore an outdated market price. - try { - offer.checkTradePriceTolerance(request.getTakersTradePrice()); - } catch (TradePriceOutOfToleranceException e) { - log.warn("Trade price check failed because takers price is outside out tolerance."); - availabilityResult = AvailabilityResult.PRICE_OUT_OF_TOLERANCE; - } catch (MarketPriceNotAvailableException e) { - log.warn(e.getMessage()); - availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE; - } catch (Throwable e) { - log.warn("Trade price check failed. " + e.getMessage()); - availabilityResult = AvailabilityResult.UNKNOWN_FAILURE; + mediatorNodeAddress = DisputeAgentSelection.getLeastUsedMediator(tradeStatisticsManager, mediatorManager).getNodeAddress(); + openOffer.setMediatorNodeAddress(mediatorNodeAddress); + Capabilities supportedCapabilities = request.getSupportedCapabilities(); + if (!OfferRestrictions.requiresUpdate() || + (supportedCapabilities != null && + Capabilities.hasMandatoryCapability(supportedCapabilities, Capability.MEDIATION))) { + try { + // Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference + // in trade price between the peers. Also here poor connectivity might cause market price API connection + // losses and therefore an outdated market price. + offer.checkTradePriceTolerance(request.getTakersTradePrice()); + } catch (TradePriceOutOfToleranceException e) { + log.warn("Trade price check failed because takers price is outside out tolerance."); + availabilityResult = AvailabilityResult.PRICE_OUT_OF_TOLERANCE; + } catch (MarketPriceNotAvailableException e) { + log.warn(e.getMessage()); + availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE; + } catch (Throwable e) { + log.warn("Trade price check failed. " + e.getMessage()); + availabilityResult = AvailabilityResult.UNKNOWN_FAILURE; + } + } else { + log.warn("Taker has not mandatory capability MEDIATION"); + // Because an old peer has not AvailabilityResult.MISSING_MANDATORY_CAPABILITY and we + // have not set the UNDEFINED fallback in AvailabilityResult the user will get a null value. + availabilityResult = AvailabilityResult.MISSING_MANDATORY_CAPABILITY; } } else { log.warn("acceptedArbitrators is null or empty: acceptedArbitrators=" + acceptedArbitrators); @@ -597,7 +631,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe availabilityResult = AvailabilityResult.OFFER_TAKEN; } - OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId, availabilityResult, arbitratorNodeAddress); + OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId, + availabilityResult, + arbitratorNodeAddress, + mediatorNodeAddress); log.info("Send {} with offerId {} and uid {} to peer {}", offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(), offerAvailabilityResponse.getUid(), peer); @@ -608,14 +645,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe @Override public void onArrived() { log.info("{} arrived at peer: offerId={}; uid={}", - offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(), offerAvailabilityResponse.getUid()); + offerAvailabilityResponse.getClass().getSimpleName(), + offerAvailabilityResponse.getOfferId(), + offerAvailabilityResponse.getUid()); } @Override public void onFault(String errorMessage) { log.error("Sending {} failed: uid={}; peer={}; error={}", - offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getUid(), - peer, errorMessage); + offerAvailabilityResponse.getClass().getSimpleName(), + offerAvailabilityResponse.getUid(), + peer, + errorMessage); } }); result = true; @@ -628,7 +669,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } - private void sendAckMessage(OfferAvailabilityRequest message, NodeAddress sender, boolean result, String errorMessage) { + private void sendAckMessage(OfferAvailabilityRequest message, + NodeAddress sender, + boolean result, + String errorMessage) { String offerId = message.getOfferId(); String sourceUid = message.getUid(); AckMessage ackMessage = new AckMessage(p2PService.getNetworkNode().getNodeAddress(), @@ -664,6 +708,98 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } + /////////////////////////////////////////////////////////////////////////////////////////// + // Update persisted offer if a new capability is required after a software update + /////////////////////////////////////////////////////////////////////////////////////////// + + private void maybeUpdatePersistedOffers() { + // We need to clone to avoid ConcurrentModificationException + ArrayList openOffersClone = new ArrayList<>(openOffers.getList()); + openOffersClone.forEach(openOffer -> { + Offer originalOffer = openOffer.getOffer(); + + OfferPayload originalOfferPayload = originalOffer.getOfferPayload(); + // We added CAPABILITIES with entry for Capability.MEDIATION in v1.1.6 and want to rewrite a + // persisted offer after the user has updated to 1.1.6 so their offer will be accepted by the network. + + if (!OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.MEDIATION)) { + // We rewrite our offer with the additional capabilities entry + + Map originalExtraDataMap = originalOfferPayload.getExtraDataMap(); + Map updatedExtraDataMap = new HashMap<>(); + + if (originalExtraDataMap != null) { + updatedExtraDataMap.putAll(originalExtraDataMap); + } + + // We overwrite any entry with our current capabilities + updatedExtraDataMap.put(OfferPayload.CAPABILITIES, Capabilities.app.toStringList()); + + OfferPayload updatedPayload = new OfferPayload(originalOfferPayload.getId(), + originalOfferPayload.getDate(), + originalOfferPayload.getOwnerNodeAddress(), + originalOfferPayload.getPubKeyRing(), + originalOfferPayload.getDirection(), + originalOfferPayload.getPrice(), + originalOfferPayload.getMarketPriceMargin(), + originalOfferPayload.isUseMarketBasedPrice(), + originalOfferPayload.getAmount(), + originalOfferPayload.getMinAmount(), + originalOfferPayload.getBaseCurrencyCode(), + originalOfferPayload.getCounterCurrencyCode(), + originalOfferPayload.getArbitratorNodeAddresses(), + originalOfferPayload.getMediatorNodeAddresses(), + originalOfferPayload.getPaymentMethodId(), + originalOfferPayload.getMakerPaymentAccountId(), + originalOfferPayload.getOfferFeePaymentTxId(), + originalOfferPayload.getCountryCode(), + originalOfferPayload.getAcceptedCountryCodes(), + originalOfferPayload.getBankId(), + originalOfferPayload.getAcceptedBankIds(), + originalOfferPayload.getVersionNr(), + originalOfferPayload.getBlockHeightAtOfferCreation(), + originalOfferPayload.getTxFee(), + originalOfferPayload.getMakerFee(), + originalOfferPayload.isCurrencyForMakerFeeBtc(), + originalOfferPayload.getBuyerSecurityDeposit(), + originalOfferPayload.getSellerSecurityDeposit(), + originalOfferPayload.getMaxTradeLimit(), + originalOfferPayload.getMaxTradePeriod(), + originalOfferPayload.isUseAutoClose(), + originalOfferPayload.isUseReOpenAfterAutoClose(), + originalOfferPayload.getLowerClosePrice(), + originalOfferPayload.getUpperClosePrice(), + originalOfferPayload.isPrivateOffer(), + originalOfferPayload.getHashOfChallenge(), + updatedExtraDataMap, + originalOfferPayload.getProtocolVersion()); + + // Save states from original data to use the for updated + Offer.State originalOfferState = originalOffer.getState(); + OpenOffer.State originalOpenOfferState = openOffer.getState(); + + // remove old offer + originalOffer.setState(Offer.State.REMOVED); + openOffer.setState(OpenOffer.State.CANCELED); + openOffer.setStorage(openOfferTradableListStorage); + openOffers.remove(openOffer); + + // Create new Offer + Offer updatedOffer = new Offer(updatedPayload); + updatedOffer.setPriceFeedService(priceFeedService); + updatedOffer.setState(originalOfferState); + + OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, openOfferTradableListStorage); + updatedOpenOffer.setState(originalOpenOfferState); + updatedOpenOffer.setStorage(openOfferTradableListStorage); + openOffers.add(updatedOpenOffer); + + log.info("Converted offer to support new Capability.MEDIATION capability. id={}", originalOffer.getId()); + } + }); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // RepublishOffers, refreshOffers /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/offer/availability/ArbitratorSelection.java b/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java similarity index 50% rename from core/src/main/java/bisq/core/offer/availability/ArbitratorSelection.java rename to core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java index 4f34cefab0..bce44e6cbc 100644 --- a/core/src/main/java/bisq/core/offer/availability/ArbitratorSelection.java +++ b/core/src/main/java/bisq/core/offer/availability/DisputeAgentSelection.java @@ -17,13 +17,15 @@ package bisq.core.offer.availability; -import bisq.core.arbitration.Arbitrator; -import bisq.core.arbitration.ArbitratorManager; +import bisq.core.support.dispute.agent.DisputeAgent; +import bisq.core.support.dispute.agent.DisputeAgentManager; import bisq.core.trade.statistics.TradeStatistics2; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.common.util.Tuple2; +import com.google.common.annotations.VisibleForTesting; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -39,10 +41,25 @@ import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkArgument; @Slf4j -public class ArbitratorSelection { +public class DisputeAgentSelection { - public static Arbitrator getLeastUsedArbitrator(TradeStatisticsManager tradeStatisticsManager, - ArbitratorManager arbitratorManager) { + public static T getLeastUsedArbitrator(TradeStatisticsManager tradeStatisticsManager, + DisputeAgentManager disputeAgentManager) { + return getLeastUsedDisputeAgent(tradeStatisticsManager, + disputeAgentManager, + TradeStatistics2.ARBITRATOR_ADDRESS); + } + + public static T getLeastUsedMediator(TradeStatisticsManager tradeStatisticsManager, + DisputeAgentManager disputeAgentManager) { + return getLeastUsedDisputeAgent(tradeStatisticsManager, + disputeAgentManager, + TradeStatistics2.MEDIATOR_ADDRESS); + } + + private static T getLeastUsedDisputeAgent(TradeStatisticsManager tradeStatisticsManager, + DisputeAgentManager disputeAgentManager, + String extraMapKey) { // We take last 100 entries from trade statistics List list = new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet()); list.sort(Comparator.comparing(TradeStatistics2::getTradeDate)); @@ -52,32 +69,33 @@ public class ArbitratorSelection { list = list.subList(0, max); } - // We stored only first 4 chars of arbitrators onion address + // We stored only first 4 chars of disputeAgents onion address List lastAddressesUsedInTrades = list.stream() .filter(tradeStatistics2 -> tradeStatistics2.getExtraDataMap() != null) - .map(tradeStatistics2 -> tradeStatistics2.getExtraDataMap().get(TradeStatistics2.ARBITRATOR_ADDRESS)) + .map(tradeStatistics2 -> tradeStatistics2.getExtraDataMap().get(extraMapKey)) .filter(Objects::nonNull) .collect(Collectors.toList()); - Set arbitrators = arbitratorManager.getArbitratorsObservableMap().values().stream() - .map(arbitrator -> arbitrator.getNodeAddress().getFullAddress()) + Set disputeAgents = disputeAgentManager.getObservableMap().values().stream() + .map(disputeAgent -> disputeAgent.getNodeAddress().getFullAddress()) .collect(Collectors.toSet()); - String result = getLeastUsedArbitrator(lastAddressesUsedInTrades, arbitrators); + String result = getLeastUsedDisputeAgent(lastAddressesUsedInTrades, disputeAgents); - Optional optionalArbitrator = arbitratorManager.getArbitratorsObservableMap().values().stream() + Optional optionalDisputeAgent = disputeAgentManager.getObservableMap().values().stream() .filter(e -> e.getNodeAddress().getFullAddress().equals(result)) .findAny(); - checkArgument(optionalArbitrator.isPresent(), "optionalArbitrator has to be present"); - return optionalArbitrator.get(); + checkArgument(optionalDisputeAgent.isPresent(), "optionalDisputeAgent has to be present"); + return optionalDisputeAgent.get(); } - static String getLeastUsedArbitrator(List lastAddressesUsedInTrades, Set arbitrators) { - checkArgument(!arbitrators.isEmpty(), "arbitrators must not be empty"); - List> arbitratorTuples = arbitrators.stream() + @VisibleForTesting + static String getLeastUsedDisputeAgent(List lastAddressesUsedInTrades, Set disputeAgents) { + checkArgument(!disputeAgents.isEmpty(), "disputeAgents must not be empty"); + List> disputeAgentTuples = disputeAgents.stream() .map(e -> new Tuple2<>(e, new AtomicInteger(0))) .collect(Collectors.toList()); - arbitratorTuples.forEach(tuple -> { + disputeAgentTuples.forEach(tuple -> { int count = (int) lastAddressesUsedInTrades.stream() .filter(tuple.first::startsWith) // we use only first 4 chars for comparing .mapToInt(e -> 1) @@ -85,8 +103,8 @@ public class ArbitratorSelection { tuple.second.set(count); }); - arbitratorTuples.sort(Comparator.comparing(e -> e.first)); - arbitratorTuples.sort(Comparator.comparingInt(e -> e.second.get())); - return arbitratorTuples.get(0).first; + disputeAgentTuples.sort(Comparator.comparing(e -> e.first)); + disputeAgentTuples.sort(Comparator.comparingInt(e -> e.second.get())); + return disputeAgentTuples.get(0).first; } } diff --git a/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java index 982540b54c..2b55c18813 100644 --- a/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java +++ b/core/src/main/java/bisq/core/offer/availability/OfferAvailabilityModel.java @@ -19,6 +19,8 @@ package bisq.core.offer.availability; import bisq.core.offer.Offer; import bisq.core.offer.messages.OfferAvailabilityResponse; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.User; import bisq.network.p2p.NodeAddress; @@ -41,6 +43,10 @@ public class OfferAvailabilityModel implements Model { private final P2PService p2PService; @Getter final private User user; + @Getter + private final MediatorManager mediatorManager; + @Getter + private final TradeStatisticsManager tradeStatisticsManager; private NodeAddress peerNodeAddress; // maker private OfferAvailabilityResponse message; @Nullable @@ -48,21 +54,32 @@ public class OfferAvailabilityModel implements Model { @Getter private NodeAddress selectedArbitrator; + // Added in v1.1.6 + @Nullable + @Setter + @Getter + private NodeAddress selectedMediator; + + public OfferAvailabilityModel(Offer offer, PubKeyRing pubKeyRing, P2PService p2PService, - User user) { + User user, + MediatorManager mediatorManager, + TradeStatisticsManager tradeStatisticsManager) { this.offer = offer; this.pubKeyRing = pubKeyRing; this.p2PService = p2PService; this.user = user; + this.mediatorManager = mediatorManager; + this.tradeStatisticsManager = tradeStatisticsManager; } public NodeAddress getPeerNodeAddress() { return peerNodeAddress; } - public void setPeerNodeAddress(NodeAddress peerNodeAddress) { + void setPeerNodeAddress(NodeAddress peerNodeAddress) { this.peerNodeAddress = peerNodeAddress; } diff --git a/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java b/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java index 2fddd54e62..1690ddc34a 100644 --- a/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java +++ b/core/src/main/java/bisq/core/offer/availability/tasks/ProcessOfferAvailabilityResponse.java @@ -19,24 +19,18 @@ package bisq.core.offer.availability.tasks; import bisq.core.offer.AvailabilityResult; import bisq.core.offer.Offer; +import bisq.core.offer.availability.DisputeAgentSelection; import bisq.core.offer.availability.OfferAvailabilityModel; import bisq.core.offer.messages.OfferAvailabilityResponse; -import bisq.core.trade.protocol.ArbitratorSelectionRule; import bisq.network.p2p.NodeAddress; import bisq.common.taskrunner.Task; import bisq.common.taskrunner.TaskRunner; -import com.google.common.collect.Lists; - -import java.util.List; -import java.util.stream.Collectors; - import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class ProcessOfferAvailabilityResponse extends Task { @@ -62,43 +56,14 @@ public class ProcessOfferAvailabilityResponse extends Task userArbitratorAddresses = model.getUser().getAcceptedArbitratorAddresses(); - checkNotNull(userArbitratorAddresses, "model.getUser().getAcceptedArbitratorAddresses() must not be null"); - - List offerArbitratorNodeAddresses = offer.getArbitratorNodeAddresses(); - - List matchingArbitrators = offerArbitratorNodeAddresses.stream() - .filter(userArbitratorAddresses::contains) - .collect(Collectors.toList()); - - if (!matchingArbitrators.isEmpty()) { - // We have at least one arbitrator which was used in the offer and is still available. - try { - model.setSelectedArbitrator(ArbitratorSelectionRule.select(Lists.newArrayList(matchingArbitrators), offer)); - complete(); - } catch (Throwable t) { - failed("There is no arbitrator matching that offer. The maker has " + - "not updated to the latest version and the arbitrators selected for that offer are not available anymore."); - } - } else { - // If an arbitrator which was selected in the offer from an old version has revoked we would get a failed take-offer attempt - // with lost trade fees. To avoid that we fail here after 1 week after the new rule is activated. - // Because one arbitrator need to revoke his application and register new as he gets too many transactions already - // we need to handle the planned revoke case. - failed("You cannot take that offer because the maker has not updated to version 0.9."); + model.setSelectedArbitrator(offerAvailabilityResponse.getArbitrator()); + NodeAddress mediator = offerAvailabilityResponse.getMediator(); + if (mediator == null) { + // We do not get a mediator from old clients so we need to handle the null case. + mediator = DisputeAgentSelection.getLeastUsedMediator(model.getTradeStatisticsManager(), model.getMediatorManager()).getNodeAddress(); } + model.setSelectedMediator(mediator); + complete(); } catch (Throwable t) { offer.setErrorMessage("An error occurred.\n" + "Error message:\n" diff --git a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java index 5cedcdc4aa..704f73c981 100644 --- a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java +++ b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityRequest.java @@ -28,6 +28,7 @@ import java.util.UUID; import lombok.EqualsAndHashCode; import lombok.Value; +import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @@ -35,6 +36,7 @@ import javax.annotation.Nullable; // to the trading peer @EqualsAndHashCode(callSuper = true) @Value +@Slf4j public final class OfferAvailabilityRequest extends OfferMessage implements SupportedCapabilitiesMessage { private final PubKeyRing pubKeyRing; private final long takersTradePrice; diff --git a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityResponse.java b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityResponse.java index 1bb7b13829..f71713c7a0 100644 --- a/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityResponse.java +++ b/core/src/main/java/bisq/core/offer/messages/OfferAvailabilityResponse.java @@ -44,17 +44,22 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup @Nullable private final Capabilities supportedCapabilities; - // Was introduced in v 0.9.0. Might be null if msg received from node with old version - @Nullable private final NodeAddress arbitrator; + // Was introduced in v 1.1.6. Might be null if msg received from node with old version + @Nullable + private final NodeAddress mediator; - public OfferAvailabilityResponse(String offerId, AvailabilityResult availabilityResult, NodeAddress arbitrator) { + public OfferAvailabilityResponse(String offerId, + AvailabilityResult availabilityResult, + NodeAddress arbitrator, + NodeAddress mediator) { this(offerId, availabilityResult, Capabilities.app, Version.getP2PMessageVersion(), UUID.randomUUID().toString(), - arbitrator); + arbitrator, + mediator); } @@ -67,22 +72,25 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup @Nullable Capabilities supportedCapabilities, int messageVersion, @Nullable String uid, - @Nullable NodeAddress arbitrator) { + NodeAddress arbitrator, + @Nullable NodeAddress mediator) { super(messageVersion, offerId, uid); this.availabilityResult = availabilityResult; this.supportedCapabilities = supportedCapabilities; this.arbitrator = arbitrator; + this.mediator = mediator; } @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { final protobuf.OfferAvailabilityResponse.Builder builder = protobuf.OfferAvailabilityResponse.newBuilder() .setOfferId(offerId) - .setAvailabilityResult(protobuf.AvailabilityResult.valueOf(availabilityResult.name())); + .setAvailabilityResult(protobuf.AvailabilityResult.valueOf(availabilityResult.name())) + .setArbitrator(arbitrator.toProtoMessage()); Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities))); Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid)); - Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator(arbitrator.toProtoMessage())); + Optional.ofNullable(mediator).ifPresent(e -> builder.setMediator(mediator.toProtoMessage())); return getNetworkEnvelopeBuilder() .setOfferAvailabilityResponse(builder) @@ -95,6 +103,7 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup Capabilities.fromIntList(proto.getSupportedCapabilitiesList()), messageVersion, proto.getUid().isEmpty() ? null : proto.getUid(), - proto.hasArbitrator() ? NodeAddress.fromProto(proto.getArbitrator()) : null); + NodeAddress.fromProto(proto.getArbitrator()), + proto.hasMediator() ? NodeAddress.fromProto(proto.getMediator()) : null); } } diff --git a/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferModel.java b/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferModel.java index 6c67acf39d..e63e31ecdf 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferModel.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/PlaceOfferModel.java @@ -17,12 +17,12 @@ package bisq.core.offer.placeoffer; -import bisq.core.arbitration.ArbitratorManager; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.offer.Offer; import bisq.core.offer.OfferBookService; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.User; diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/CreateMakerFeeTx.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/CreateMakerFeeTx.java index 6419a0b287..f7e35d82a0 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/tasks/CreateMakerFeeTx.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/CreateMakerFeeTx.java @@ -17,7 +17,6 @@ package bisq.core.offer.placeoffer.tasks; -import bisq.core.arbitration.Arbitrator; import bisq.core.btc.exceptions.TxBroadcastException; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BsqWalletService; @@ -28,8 +27,9 @@ import bisq.core.btc.wallet.WalletService; import bisq.core.dao.exceptions.DaoDisabledException; import bisq.core.dao.state.model.blockchain.TxType; import bisq.core.offer.Offer; -import bisq.core.offer.availability.ArbitratorSelection; +import bisq.core.offer.availability.DisputeAgentSelection; import bisq.core.offer.placeoffer.PlaceOfferModel; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.common.UserThread; import bisq.common.taskrunner.Task; @@ -47,7 +47,7 @@ public class CreateMakerFeeTx extends Task { private static final Logger log = LoggerFactory.getLogger(CreateMakerFeeTx.class); private Transaction tradeFeeTx = null; - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public CreateMakerFeeTx(TaskRunner taskHandler, PlaceOfferModel model) { super(taskHandler, model); } @@ -62,7 +62,7 @@ public class CreateMakerFeeTx extends Task { String id = offer.getId(); BtcWalletService walletService = model.getWalletService(); - Arbitrator arbitrator = ArbitratorSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), + Arbitrator arbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(), model.getArbitratorManager()); Address fundingAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.OFFER_FUNDING).getAddress(); diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/ValidateOffer.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/ValidateOffer.java index 8216c924f2..70701adc79 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/tasks/ValidateOffer.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/ValidateOffer.java @@ -83,8 +83,6 @@ public class ValidateOffer extends Task { checkArgument(offer.getDate().getTime() > 0, "Date must not be 0. date=" + offer.getDate().toString()); - checkNotNull(offer.getArbitratorNodeAddresses(), "Arbitrator is null"); - checkNotNull(offer.getMediatorNodeAddresses(), "Mediator is null"); checkNotNull(offer.getCurrencyCode(), "Currency is null"); checkNotNull(offer.getDirection(), "Direction is null"); checkNotNull(offer.getId(), "Id is null"); diff --git a/core/src/main/java/bisq/core/payment/payload/CountryBasedPaymentAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/CountryBasedPaymentAccountPayload.java index e926099113..80aafc0bec 100644 --- a/core/src/main/java/bisq/core/payment/payload/CountryBasedPaymentAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/CountryBasedPaymentAccountPayload.java @@ -65,9 +65,9 @@ public abstract class CountryBasedPaymentAccountPayload extends PaymentAccountPa .setCountryBasedPaymentAccountPayload(builder); } - abstract public String getPaymentDetails(); + public abstract String getPaymentDetails(); - abstract public String getPaymentDetailsForTradePopup(); + public abstract String getPaymentDetailsForTradePopup(); @Override protected byte[] getAgeWitnessInputData(byte[] data) { diff --git a/core/src/main/java/bisq/core/payment/payload/PaymentAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/PaymentAccountPayload.java index 5852aa618b..fb771f7aa1 100644 --- a/core/src/main/java/bisq/core/payment/payload/PaymentAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/PaymentAccountPayload.java @@ -118,9 +118,9 @@ public abstract class PaymentAccountPayload implements NetworkPayload, UsedForTr // API /////////////////////////////////////////////////////////////////////////////////////////// - abstract public String getPaymentDetails(); + public abstract String getPaymentDetails(); - abstract public String getPaymentDetailsForTradePopup(); + public abstract String getPaymentDetailsForTradePopup(); public byte[] getSalt() { checkNotNull(excludeFromJsonDataMap, "excludeFromJsonDataMap must not be null"); diff --git a/core/src/main/java/bisq/core/presentation/CorePresentationModule.java b/core/src/main/java/bisq/core/presentation/CorePresentationModule.java index 6d84520d3e..2d0b804886 100644 --- a/core/src/main/java/bisq/core/presentation/CorePresentationModule.java +++ b/core/src/main/java/bisq/core/presentation/CorePresentationModule.java @@ -32,7 +32,7 @@ public class CorePresentationModule extends AppModule { protected void configure() { bind(BalancePresentation.class).in(Singleton.class); bind(TradePresentation.class).in(Singleton.class); - bind(DisputePresentation.class).in(Singleton.class); + bind(SupportTicketsPresentation.class).in(Singleton.class); } } diff --git a/core/src/main/java/bisq/core/presentation/DisputePresentation.java b/core/src/main/java/bisq/core/presentation/DisputePresentation.java deleted file mode 100644 index ab79175b99..0000000000 --- a/core/src/main/java/bisq/core/presentation/DisputePresentation.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - -package bisq.core.presentation; - -import bisq.core.arbitration.DisputeManager; - -import javax.inject.Inject; - -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; - -import lombok.Getter; - -public class DisputePresentation { - @Getter - private final StringProperty numOpenDisputes = new SimpleStringProperty(); - @Getter - private final BooleanProperty showOpenDisputesNotification = new SimpleBooleanProperty(); - - @Inject - public DisputePresentation(DisputeManager disputeManager) { - disputeManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> { - int openDisputes = (int) newValue; - if (openDisputes > 0) - numOpenDisputes.set(String.valueOf(openDisputes)); - if (openDisputes > 9) - numOpenDisputes.set("★"); - - showOpenDisputesNotification.set(openDisputes > 0); - }); - } -} diff --git a/core/src/main/java/bisq/core/presentation/SupportTicketsPresentation.java b/core/src/main/java/bisq/core/presentation/SupportTicketsPresentation.java new file mode 100644 index 0000000000..be98548140 --- /dev/null +++ b/core/src/main/java/bisq/core/presentation/SupportTicketsPresentation.java @@ -0,0 +1,78 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.presentation; + +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.mediation.MediationManager; + +import javax.inject.Inject; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import java.util.concurrent.atomic.AtomicInteger; + +import lombok.Getter; + +public class SupportTicketsPresentation { + @Getter + private final StringProperty numOpenArbitrationTickets = new SimpleStringProperty(); + @Getter + private final BooleanProperty showOpenArbitrationTicketsNotification = new SimpleBooleanProperty(); + + @Getter + private final StringProperty numOpenMediationTickets = new SimpleStringProperty(); + @Getter + private final BooleanProperty showOpenMediationTicketsNotification = new SimpleBooleanProperty(); + + @Getter + private final StringProperty numOpenSupportTickets = new SimpleStringProperty(); + @Getter + private final BooleanProperty showOpenSupportTicketsNotification = new SimpleBooleanProperty(); + @org.jetbrains.annotations.NotNull + private final ArbitrationManager arbitrationManager; + @org.jetbrains.annotations.NotNull + private final MediationManager mediationManager; + + @Inject + public SupportTicketsPresentation(ArbitrationManager arbitrationManager, MediationManager mediationManager) { + this.arbitrationManager = arbitrationManager; + this.mediationManager = mediationManager; + + arbitrationManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange()); + mediationManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange()); + } + + private void onChange() { + AtomicInteger openArbitrationDisputes = new AtomicInteger(arbitrationManager.getNumOpenDisputes().get()); + int arbitrationTickets = openArbitrationDisputes.get(); + numOpenArbitrationTickets.set(String.valueOf(arbitrationTickets)); + showOpenArbitrationTicketsNotification.set(arbitrationTickets > 0); + + AtomicInteger openMediationDisputes = new AtomicInteger(mediationManager.getNumOpenDisputes().get()); + int mediationTickets = openMediationDisputes.get(); + numOpenMediationTickets.set(String.valueOf(mediationTickets)); + showOpenMediationTicketsNotification.set(mediationTickets > 0); + + int supportTickets = arbitrationTickets + mediationTickets; + numOpenSupportTickets.set(String.valueOf(supportTickets)); + showOpenSupportTicketsNotification.set(supportTickets > 0); + } +} diff --git a/core/src/main/java/bisq/core/proto/ProtoDevUtil.java b/core/src/main/java/bisq/core/proto/ProtoDevUtil.java index c62b65e0ea..6b70441a86 100644 --- a/core/src/main/java/bisq/core/proto/ProtoDevUtil.java +++ b/core/src/main/java/bisq/core/proto/ProtoDevUtil.java @@ -17,8 +17,8 @@ package bisq.core.proto; -import bisq.core.arbitration.DisputeResult; import bisq.core.btc.model.AddressEntry; +import bisq.core.support.dispute.DisputeResult; import bisq.core.offer.AvailabilityResult; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; diff --git a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java index 3d0c065fc4..9d84408420 100644 --- a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -19,13 +19,6 @@ package bisq.core.proto.network; import bisq.core.alert.Alert; import bisq.core.alert.PrivateNotificationMessage; -import bisq.core.arbitration.Arbitrator; -import bisq.core.arbitration.Mediator; -import bisq.core.arbitration.messages.DisputeCommunicationMessage; -import bisq.core.arbitration.messages.DisputeResultMessage; -import bisq.core.arbitration.messages.OpenNewDisputeMessage; -import bisq.core.arbitration.messages.PeerOpenedDisputeMessage; -import bisq.core.arbitration.messages.PeerPublishedDisputePayoutTxMessage; import bisq.core.dao.governance.blindvote.network.messages.RepublishGovernanceDataRequest; import bisq.core.dao.governance.proposal.storage.temp.TempProposalPayload; import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest; @@ -45,8 +38,17 @@ import bisq.core.offer.OfferPayload; import bisq.core.offer.messages.OfferAvailabilityRequest; import bisq.core.offer.messages.OfferAvailabilityResponse; import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.arbitration.messages.PeerPublishedDisputePayoutTxMessage; +import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.support.dispute.messages.DisputeResultMessage; +import bisq.core.support.dispute.messages.OpenNewDisputeMessage; +import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.messages.ChatMessage; import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; import bisq.core.trade.messages.DepositTxPublishedMessage; +import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; +import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; import bisq.core.trade.messages.PayDepositRequest; import bisq.core.trade.messages.PayoutTxPublishedMessage; import bisq.core.trade.messages.PublishDepositTxRequest; @@ -142,13 +144,17 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo return CounterCurrencyTransferStartedMessage.fromProto(proto.getCounterCurrencyTransferStartedMessage(), messageVersion); case PAYOUT_TX_PUBLISHED_MESSAGE: return PayoutTxPublishedMessage.fromProto(proto.getPayoutTxPublishedMessage(), messageVersion); + case MEDIATED_PAYOUT_TX_SIGNATURE_MESSAGE: + return MediatedPayoutTxSignatureMessage.fromProto(proto.getMediatedPayoutTxSignatureMessage(), messageVersion); + case MEDIATED_PAYOUT_TX_PUBLISHED_MESSAGE: + return MediatedPayoutTxPublishedMessage.fromProto(proto.getMediatedPayoutTxPublishedMessage(), messageVersion); case OPEN_NEW_DISPUTE_MESSAGE: return OpenNewDisputeMessage.fromProto(proto.getOpenNewDisputeMessage(), this, messageVersion); case PEER_OPENED_DISPUTE_MESSAGE: return PeerOpenedDisputeMessage.fromProto(proto.getPeerOpenedDisputeMessage(), this, messageVersion); - case DISPUTE_COMMUNICATION_MESSAGE: - return DisputeCommunicationMessage.fromProto(proto.getDisputeCommunicationMessage(), messageVersion); + case CHAT_MESSAGE: + return ChatMessage.fromProto(proto.getChatMessage(), messageVersion); case DISPUTE_RESULT_MESSAGE: return DisputeResultMessage.fromProto(proto.getDisputeResultMessage(), messageVersion); case PEER_PUBLISHED_DISPUTE_PAYOUT_TX_MESSAGE: diff --git a/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java b/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java index dbd7ebeef5..796572b0c3 100644 --- a/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java @@ -19,7 +19,6 @@ package bisq.core.proto.persistable; import bisq.core.account.sign.SignedWitnessStore; import bisq.core.account.witness.AccountAgeWitnessStore; -import bisq.core.arbitration.DisputeList; import bisq.core.btc.model.AddressEntryList; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.dao.governance.blindvote.MyBlindVoteList; @@ -36,6 +35,8 @@ import bisq.core.dao.state.model.governance.MeritList; import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputList; import bisq.core.payment.PaymentAccountList; import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.arbitration.ArbitrationDisputeList; +import bisq.core.support.dispute.mediation.MediationDisputeList; import bisq.core.trade.TradableList; import bisq.core.trade.statistics.TradeStatistics2Store; import bisq.core.user.PreferencesPayload; @@ -101,8 +102,12 @@ public class CorePersistenceProtoResolver extends CoreProtoResolver implements P btcWalletService.get()); case TRADE_STATISTICS_LIST: throw new ProtobufferRuntimeException("TRADE_STATISTICS_LIST is not used anymore"); - case DISPUTE_LIST: - return DisputeList.fromProto(proto.getDisputeList(), + case ARBITRATION_DISPUTE_LIST: + return ArbitrationDisputeList.fromProto(proto.getArbitrationDisputeList(), + this, + new Storage<>(storageDir, this, corruptedDatabaseFilesHandler)); + case MEDIATION_DISPUTE_LIST: + return MediationDisputeList.fromProto(proto.getMediationDisputeList(), this, new Storage<>(storageDir, this, corruptedDatabaseFilesHandler)); case PREFERENCES_PAYLOAD: diff --git a/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java b/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java index 691f1eecba..757f1f09df 100644 --- a/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java +++ b/core/src/main/java/bisq/core/setup/CoreNetworkCapabilities.java @@ -27,7 +27,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class CoreNetworkCapabilities { - public static void setSupportedCapabilities(BisqEnvironment bisqEnvironment) { + static void setSupportedCapabilities(BisqEnvironment bisqEnvironment) { Capabilities.app.addAll( Capability.TRADE_STATISTICS, Capability.TRADE_STATISTICS_2, @@ -36,7 +36,8 @@ public class CoreNetworkCapabilities { Capability.PROPOSAL, Capability.BLIND_VOTE, Capability.DAO_STATE, - Capability.BUNDLE_OF_ENVELOPES + Capability.BUNDLE_OF_ENVELOPES, + Capability.MEDIATION ); if (BisqEnvironment.isDaoActivated(bisqEnvironment)) { diff --git a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java index 92892109cd..1a068e82a8 100644 --- a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java +++ b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java @@ -17,7 +17,6 @@ package bisq.core.setup; -import bisq.core.arbitration.DisputeManager; import bisq.core.btc.model.AddressEntryList; import bisq.core.dao.DaoOptionKeys; import bisq.core.dao.governance.ballot.BallotListService; @@ -28,6 +27,8 @@ import bisq.core.dao.governance.proofofburn.MyProofOfBurnListService; import bisq.core.dao.governance.proposal.MyProposalListService; import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService; import bisq.core.offer.OpenOfferManager; +import bisq.core.support.dispute.arbitration.ArbitrationDisputeListService; +import bisq.core.support.dispute.mediation.MediationDisputeListService; import bisq.core.trade.TradeManager; import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.failed.FailedTradesManager; @@ -60,7 +61,8 @@ public class CorePersistedDataHost { persistedDataHosts.add(injector.getInstance(TradeManager.class)); persistedDataHosts.add(injector.getInstance(ClosedTradableManager.class)); persistedDataHosts.add(injector.getInstance(FailedTradesManager.class)); - persistedDataHosts.add(injector.getInstance(DisputeManager.class)); + persistedDataHosts.add(injector.getInstance(ArbitrationDisputeListService.class)); + persistedDataHosts.add(injector.getInstance(MediationDisputeListService.class)); persistedDataHosts.add(injector.getInstance(P2PService.class)); if (injector.getInstance(Key.get(Boolean.class, Names.named(DaoOptionKeys.DAO_ACTIVATED)))) { diff --git a/core/src/main/java/bisq/core/support/SupportManager.java b/core/src/main/java/bisq/core/support/SupportManager.java new file mode 100644 index 0000000000..908a594aee --- /dev/null +++ b/core/src/main/java/bisq/core/support/SupportManager.java @@ -0,0 +1,320 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.support.messages.ChatMessage; +import bisq.core.support.messages.SupportMessage; + +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.common.Timer; +import bisq.common.UserThread; +import bisq.common.crypto.PubKeyRing; +import bisq.common.proto.network.NetworkEnvelope; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArraySet; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public abstract class SupportManager { + protected final P2PService p2PService; + protected final WalletsSetup walletsSetup; + protected final Map delayMsgMap = new HashMap<>(); + private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); + private final CopyOnWriteArraySet decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>(); + private boolean allServicesInitialized; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public SupportManager(P2PService p2PService, WalletsSetup walletsSetup) { + this.p2PService = p2PService; + this.walletsSetup = walletsSetup; + + // We get first the message handler called then the onBootstrapped + p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> { + decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey); + tryApplyMessages(); + }); + p2PService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> { + decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey); + tryApplyMessages(); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract methods + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract void dispatchMessage(SupportMessage networkEnvelope); + + public abstract NodeAddress getPeerNodeAddress(ChatMessage message); + + public abstract PubKeyRing getPeerPubKeyRing(ChatMessage message); + + public abstract SupportType getSupportType(); + + public abstract boolean channelOpen(ChatMessage message); + + public abstract List getAllChatMessages(); + + public abstract void addAndPersistChatMessage(ChatMessage message); + + public abstract void persist(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Delegates p2pService + /////////////////////////////////////////////////////////////////////////////////////////// + + public boolean isBootstrapped() { + return p2PService.isBootstrapped(); + } + + public NodeAddress getMyAddress() { + return p2PService.getAddress(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + allServicesInitialized = true; + } + + public void tryApplyMessages() { + if (isReady()) + applyMessages(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Message handler + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void onChatMessage(ChatMessage chatMessage) { + final String tradeId = chatMessage.getTradeId(); + final String uid = chatMessage.getUid(); + boolean channelOpen = channelOpen(chatMessage); + if (!channelOpen) { + log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId); + if (!delayMsgMap.containsKey(uid)) { + Timer timer = UserThread.runAfter(() -> onChatMessage(chatMessage), 1); + delayMsgMap.put(uid, timer); + } else { + String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId; + log.warn(msg); + } + return; + } + + cleanupRetryMap(uid); + PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(chatMessage); + + addAndPersistChatMessage(chatMessage); + + // We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we + // cannot send it anyway) + if (receiverPubKeyRing != null) + sendAckMessage(chatMessage, receiverPubKeyRing, true, null); + } + + private void onAckMessage(AckMessage ackMessage, + @Nullable DecryptedMessageWithPubKey decryptedMessageWithPubKey) { + if (ackMessage.getSourceType() == getAckMessageSourceType()) { + if (ackMessage.isSuccess()) { + log.info("Received AckMessage for {} with tradeId {} and uid {}", + ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid()); + } else { + log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}", + ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage()); + } + + getAllChatMessages().stream() + .filter(msg -> msg.getUid().equals(ackMessage.getSourceUid())) + .forEach(msg -> { + if (ackMessage.isSuccess()) + msg.setAcknowledged(true); + else + msg.setAckError(ackMessage.getErrorMessage()); + }); + persist(); + + if (decryptedMessageWithPubKey != null) + p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey); + } + } + + protected abstract AckMessageSourceType getAckMessageSourceType(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Send message + /////////////////////////////////////////////////////////////////////////////////////////// + + public ChatMessage sendChatMessage(ChatMessage message) { + NodeAddress peersNodeAddress = getPeerNodeAddress(message); + PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(message); + if (receiverPubKeyRing != null) { + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + + p2PService.sendEncryptedMailboxMessage(peersNodeAddress, + receiverPubKeyRing, + message, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + message.setArrived(true); + persist(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + message.setStoredInMailbox(true); + persist(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + message.setSendMessageError(errorMessage); + persist(); + } + } + ); + } + + return message; + } + + protected void sendAckMessage(SupportMessage supportMessage, PubKeyRing peersPubKeyRing, + boolean result, @Nullable String errorMessage) { + String tradeId = supportMessage.getTradeId(); + String uid = supportMessage.getUid(); + AckMessage ackMessage = new AckMessage(p2PService.getNetworkNode().getNodeAddress(), + getAckMessageSourceType(), + supportMessage.getClass().getSimpleName(), + uid, + tradeId, + result, + errorMessage); + final NodeAddress peersNodeAddress = supportMessage.getSenderNodeAddress(); + log.info("Send AckMessage for {} to peer {}. tradeId={}, uid={}", + ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); + p2PService.sendEncryptedMailboxMessage( + peersNodeAddress, + peersPubKeyRing, + ackMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("AckMessage for {} arrived at peer {}. tradeId={}, uid={}", + ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); + } + + @Override + public void onStoredInMailbox() { + log.info("AckMessage for {} stored in mailbox for peer {}. tradeId={}, uid={}", + ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid); + } + + @Override + public void onFault(String errorMessage) { + log.error("AckMessage for {} failed. Peer {}. tradeId={}, uid={}, errorMessage={}", + ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid, errorMessage); + } + } + ); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + protected boolean canProcessMessage(SupportMessage message) { + return message.getSupportType() == getSupportType(); + } + + protected void cleanupRetryMap(String uid) { + if (delayMsgMap.containsKey(uid)) { + Timer timer = delayMsgMap.remove(uid); + if (timer != null) + timer.stop(); + } + } + + private boolean isReady() { + return allServicesInitialized && + p2PService.isBootstrapped() && + walletsSetup.isDownloadComplete() && + walletsSetup.hasSufficientPeersForBroadcast(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void applyMessages() { + decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + if (networkEnvelope instanceof SupportMessage) { + dispatchMessage((SupportMessage) networkEnvelope); + } else if (networkEnvelope instanceof AckMessage) { + onAckMessage((AckMessage) networkEnvelope, null); + } + }); + decryptedDirectMessageWithPubKeys.clear(); + + decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> { + NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); + log.debug("decryptedMessageWithPubKey.message " + networkEnvelope); + if (networkEnvelope instanceof SupportMessage) { + dispatchMessage((SupportMessage) networkEnvelope); + p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey); + } else if (networkEnvelope instanceof AckMessage) { + onAckMessage((AckMessage) networkEnvelope, decryptedMessageWithPubKey); + } + }); + decryptedMailboxMessageWithPubKeys.clear(); + } +} diff --git a/core/src/main/java/bisq/core/support/SupportSession.java b/core/src/main/java/bisq/core/support/SupportSession.java new file mode 100644 index 0000000000..b4df4ed0b1 --- /dev/null +++ b/core/src/main/java/bisq/core/support/SupportSession.java @@ -0,0 +1,56 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support; + +import bisq.core.support.messages.ChatMessage; + +import bisq.common.crypto.PubKeyRing; + +import javafx.collections.ObservableList; + +/** + * A Support session is using a trade or a dispute to implement the methods. + * It keeps the ChatView transparent if used in dispute or trade chat context. + */ +public abstract class SupportSession { + // todo refactor ui so that can be converted to isTrader + private boolean isClient; + + + protected SupportSession(boolean isClient) { + this.isClient = isClient; + } + + protected SupportSession() { + } + + // todo refactor ui so that can be converted to isTrader + public boolean isClient() { + return isClient; + } + + public abstract String getTradeId(); + + public abstract PubKeyRing getClientPubKeyRing(); + + public abstract ObservableList getObservableChatMessageList(); + + public abstract boolean chatIsOpen(); + + public abstract boolean isDisputeAgent(); +} diff --git a/core/src/main/java/bisq/core/arbitration/ArbitratorModule.java b/core/src/main/java/bisq/core/support/SupportType.java similarity index 57% rename from core/src/main/java/bisq/core/arbitration/ArbitratorModule.java rename to core/src/main/java/bisq/core/support/SupportType.java index d65760c3ab..4d13c7848e 100644 --- a/core/src/main/java/bisq/core/arbitration/ArbitratorModule.java +++ b/core/src/main/java/bisq/core/support/SupportType.java @@ -15,23 +15,21 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration; +package bisq.core.support; -import bisq.common.app.AppModule; +import bisq.common.proto.ProtoUtil; -import org.springframework.core.env.Environment; +public enum SupportType { + ARBITRATION, // Need to be at index 0 to be the fall back for old clients + MEDIATION, + TRADE; -import com.google.inject.Singleton; - -public class ArbitratorModule extends AppModule { - public ArbitratorModule(Environment environment) { - super(environment); + public static SupportType fromProto( + protobuf.SupportType type) { + return ProtoUtil.enumFromProto(SupportType.class, type.name()); } - @Override - protected final void configure() { - bind(ArbitratorManager.class).in(Singleton.class); - bind(DisputeManager.class).in(Singleton.class); - bind(ArbitratorService.class).in(Singleton.class); + public static protobuf.SupportType toProtoMessage(SupportType supportType) { + return protobuf.SupportType.valueOf(supportType.name()); } } diff --git a/core/src/main/java/bisq/core/arbitration/Attachment.java b/core/src/main/java/bisq/core/support/dispute/Attachment.java similarity index 97% rename from core/src/main/java/bisq/core/arbitration/Attachment.java rename to core/src/main/java/bisq/core/support/dispute/Attachment.java index ec7946575a..1f425da2bf 100644 --- a/core/src/main/java/bisq/core/arbitration/Attachment.java +++ b/core/src/main/java/bisq/core/support/dispute/Attachment.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration; +package bisq.core.support.dispute; import bisq.common.proto.network.NetworkPayload; diff --git a/core/src/main/java/bisq/core/arbitration/Dispute.java b/core/src/main/java/bisq/core/support/dispute/Dispute.java similarity index 89% rename from core/src/main/java/bisq/core/arbitration/Dispute.java rename to core/src/main/java/bisq/core/support/dispute/Dispute.java index 0266bd3e82..3348bbf963 100644 --- a/core/src/main/java/bisq/core/arbitration/Dispute.java +++ b/core/src/main/java/bisq/core/support/dispute/Dispute.java @@ -15,10 +15,11 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration; +package bisq.core.support.dispute; -import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.SupportType; +import bisq.core.support.messages.ChatMessage; import bisq.core.trade.Contract; import bisq.common.crypto.PubKeyRing; @@ -39,7 +40,9 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Optional; import java.util.stream.Collectors; @@ -77,25 +80,24 @@ public final class Dispute implements NetworkPayload { private final String makerContractSignature; @Nullable private final String takerContractSignature; - private final PubKeyRing arbitratorPubKeyRing; + private final PubKeyRing agentPubKeyRing; // arbitrator or mediator private final boolean isSupportTicket; - private final ObservableList disputeCommunicationMessages = FXCollections.observableArrayList(); + private final ObservableList chatMessages = FXCollections.observableArrayList(); private BooleanProperty isClosedProperty = new SimpleBooleanProperty(); // disputeResultProperty.get is Nullable! private ObjectProperty disputeResultProperty = new SimpleObjectProperty<>(); @Nullable private String disputePayoutTxId; - private long openingDate; - transient private Storage storage; + transient private Storage storage; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - public Dispute(Storage storage, + public Dispute(Storage storage, String tradeId, int traderId, boolean disputeOpenerIsBuyer, @@ -111,7 +113,7 @@ public final class Dispute implements NetworkPayload { String contractAsJson, @Nullable String makerContractSignature, @Nullable String takerContractSignature, - PubKeyRing arbitratorPubKeyRing, + PubKeyRing agentPubKeyRing, boolean isSupportTicket) { this(tradeId, traderId, @@ -128,7 +130,7 @@ public final class Dispute implements NetworkPayload { contractAsJson, makerContractSignature, takerContractSignature, - arbitratorPubKeyRing, + agentPubKeyRing, isSupportTicket); this.storage = storage; openingDate = new Date().getTime(); @@ -154,7 +156,7 @@ public final class Dispute implements NetworkPayload { String contractAsJson, @Nullable String makerContractSignature, @Nullable String takerContractSignature, - PubKeyRing arbitratorPubKeyRing, + PubKeyRing agentPubKeyRing, boolean isSupportTicket) { this.tradeId = tradeId; this.traderId = traderId; @@ -171,7 +173,7 @@ public final class Dispute implements NetworkPayload { this.contractAsJson = contractAsJson; this.makerContractSignature = makerContractSignature; this.takerContractSignature = takerContractSignature; - this.arbitratorPubKeyRing = arbitratorPubKeyRing; + this.agentPubKeyRing = agentPubKeyRing; this.isSupportTicket = isSupportTicket; id = tradeId + "_" + traderId; @@ -179,6 +181,8 @@ public final class Dispute implements NetworkPayload { @Override public protobuf.Dispute toProtoMessage() { + // Needed to avoid ConcurrentModificationException + List clonedChatMessages = new ArrayList<>(chatMessages); protobuf.Dispute.Builder builder = protobuf.Dispute.newBuilder() .setTradeId(tradeId) .setTraderId(traderId) @@ -188,10 +192,10 @@ public final class Dispute implements NetworkPayload { .setTradeDate(tradeDate) .setContract(contract.toProtoMessage()) .setContractAsJson(contractAsJson) - .setArbitratorPubKeyRing(arbitratorPubKeyRing.toProtoMessage()) + .setAgentPubKeyRing(agentPubKeyRing.toProtoMessage()) .setIsSupportTicket(isSupportTicket) - .addAllDisputeCommunicationMessages(disputeCommunicationMessages.stream() - .map(msg -> msg.toProtoNetworkEnvelope().getDisputeCommunicationMessage()) + .addAllChatMessage(clonedChatMessages.stream() + .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) .collect(Collectors.toList())) .setIsClosed(isClosedProperty.get()) .setOpeningDate(openingDate) @@ -225,11 +229,11 @@ public final class Dispute implements NetworkPayload { proto.getContractAsJson(), ProtoUtil.stringOrNullFromProto(proto.getMakerContractSignature()), ProtoUtil.stringOrNullFromProto(proto.getTakerContractSignature()), - PubKeyRing.fromProto(proto.getArbitratorPubKeyRing()), + PubKeyRing.fromProto(proto.getAgentPubKeyRing()), proto.getIsSupportTicket()); - dispute.disputeCommunicationMessages.addAll(proto.getDisputeCommunicationMessagesList().stream() - .map(DisputeCommunicationMessage::fromPayloadProto) + dispute.chatMessages.addAll(proto.getChatMessageList().stream() + .map(ChatMessage::fromPayloadProto) .collect(Collectors.toList())); dispute.openingDate = proto.getOpeningDate(); @@ -245,22 +249,26 @@ public final class Dispute implements NetworkPayload { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void addDisputeCommunicationMessage(DisputeCommunicationMessage disputeCommunicationMessage) { - if (!disputeCommunicationMessages.contains(disputeCommunicationMessage)) { - disputeCommunicationMessages.add(disputeCommunicationMessage); + public void addAndPersistChatMessage(ChatMessage chatMessage) { + if (!chatMessages.contains(chatMessage)) { + chatMessages.add(chatMessage); storage.queueUpForSave(); } else { log.error("disputeDirectMessage already exists"); } } + public boolean isMediationDispute() { + return !chatMessages.isEmpty() && chatMessages.get(0).getSupportType() == SupportType.MEDIATION; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Setters /////////////////////////////////////////////////////////////////////////////////////////// // In case we get the object via the network storage is not set as its transient, so we need to set it. - public void setStorage(Storage storage) { + public void setStorage(Storage storage) { this.storage = storage; } @@ -278,7 +286,6 @@ public final class Dispute implements NetworkPayload { storage.queueUpForSave(); } - @SuppressWarnings("NullableProblems") public void setDisputePayoutTxId(String disputePayoutTxId) { boolean changed = this.disputePayoutTxId == null || !this.disputePayoutTxId.equals(disputePayoutTxId); this.disputePayoutTxId = disputePayoutTxId; @@ -335,9 +342,9 @@ public final class Dispute implements NetworkPayload { ", contractAsJson='" + contractAsJson + '\'' + ", makerContractSignature='" + makerContractSignature + '\'' + ", takerContractSignature='" + takerContractSignature + '\'' + - ", arbitratorPubKeyRing=" + arbitratorPubKeyRing + + ", agentPubKeyRing=" + agentPubKeyRing + ", isSupportTicket=" + isSupportTicket + - ", disputeCommunicationMessages=" + disputeCommunicationMessages + + ", chatMessages=" + chatMessages + ", isClosed=" + isClosedProperty.get() + ", disputeResult=" + disputeResultProperty.get() + ", disputePayoutTxId='" + disputePayoutTxId + '\'' + diff --git a/core/src/main/java/bisq/core/arbitration/DisputeAlreadyOpenException.java b/core/src/main/java/bisq/core/support/dispute/DisputeAlreadyOpenException.java similarity index 95% rename from core/src/main/java/bisq/core/arbitration/DisputeAlreadyOpenException.java rename to core/src/main/java/bisq/core/support/dispute/DisputeAlreadyOpenException.java index 220080c5de..04e2cdd978 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeAlreadyOpenException.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeAlreadyOpenException.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration; +package bisq.core.support.dispute; public class DisputeAlreadyOpenException extends Exception { public DisputeAlreadyOpenException() { diff --git a/core/src/main/java/bisq/core/arbitration/DisputeList.java b/core/src/main/java/bisq/core/support/dispute/DisputeList.java similarity index 59% rename from core/src/main/java/bisq/core/arbitration/DisputeList.java rename to core/src/main/java/bisq/core/support/dispute/DisputeList.java index d1060bd019..d2dd6cca12 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeList.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeList.java @@ -15,22 +15,16 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration; +package bisq.core.support.dispute; -import bisq.core.proto.CoreProtoResolver; - -import bisq.common.proto.ProtoUtil; import bisq.common.proto.persistable.PersistableEnvelope; import bisq.common.proto.persistable.PersistedDataHost; import bisq.common.storage.Storage; -import com.google.protobuf.Message; - import javafx.collections.FXCollections; import javafx.collections.ObservableList; import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.Getter; @@ -39,53 +33,32 @@ import lombok.extern.slf4j.Slf4j; @Slf4j @ToString -/** +/* * Holds a List of Dispute objects. * * Calls to the List are delegated because this class intercepts the add/remove calls so changes * can be saved to disc. */ -public final class DisputeList implements PersistableEnvelope, PersistedDataHost { - transient private final Storage storage; - @Getter - private final ObservableList list = FXCollections.observableArrayList(); +public abstract class DisputeList implements PersistableEnvelope, PersistedDataHost { + transient protected final Storage storage; - public DisputeList(Storage storage) { + @Getter + protected final ObservableList list = FXCollections.observableArrayList(); + + public DisputeList(Storage storage) { this.storage = storage; } - @Override - public void readPersisted() { - DisputeList persisted = storage.initAndGetPersisted(this, 50); - if (persisted != null) - list.addAll(persisted.getList()); - } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private DisputeList(Storage storage, List list) { + protected DisputeList(Storage storage, List list) { this.storage = storage; this.list.addAll(list); } - @Override - public Message toProtoMessage() { - return protobuf.PersistableEnvelope.newBuilder().setDisputeList(protobuf.DisputeList.newBuilder() - .addAllDispute(ProtoUtil.collectionToProto(list))).build(); - } - - public static DisputeList fromProto(protobuf.DisputeList proto, - CoreProtoResolver coreProtoResolver, - Storage storage) { - List list = proto.getDisputeList().stream() - .map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver)) - .collect(Collectors.toList()); - list.forEach(e -> e.setStorage(storage)); - return new DisputeList(storage, list); - } - /////////////////////////////////////////////////////////////////////////////////////////// // API @@ -93,10 +66,9 @@ public final class DisputeList implements PersistableEnvelope, PersistedDataHost public boolean add(Dispute dispute) { if (!list.contains(dispute)) { - boolean changed = list.add(dispute); - if (changed) - persist(); - return changed; + list.add(dispute); + persist(); + return true; } else { return false; } @@ -122,7 +94,7 @@ public final class DisputeList implements PersistableEnvelope, PersistedDataHost return list.isEmpty(); } - @SuppressWarnings({"BooleanMethodIsAlwaysInverted", "SuspiciousMethodCalls"}) + @SuppressWarnings({"SuspiciousMethodCalls"}) public boolean contains(Object o) { return list.contains(o); } diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeListService.java b/core/src/main/java/bisq/core/support/dispute/DisputeListService.java new file mode 100644 index 0000000000..d3f57cb03a --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/DisputeListService.java @@ -0,0 +1,190 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute; + +import bisq.core.trade.Contract; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.UserThread; +import bisq.common.proto.persistable.PersistedDataHost; +import bisq.common.storage.Storage; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; + +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public abstract class DisputeListService> implements PersistedDataHost { + @Getter + protected final Storage storage; + @Nullable + @Getter + private T disputeList; + private final Map disputeIsClosedSubscriptionsMap = new HashMap<>(); + @Getter + private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public DisputeListService(Storage storage) { + this.storage = storage; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract methods + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract T getConcreteDisputeList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistedDataHost + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void readPersisted() { + disputeList = getConcreteDisputeList(); + disputeList.readPersisted(); + disputeList.stream().forEach(dispute -> dispute.setStorage(storage)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Public + /////////////////////////////////////////////////////////////////////////////////////////// + + public void cleanupDisputes(@Nullable Consumer closedDisputeHandler) { + if (disputeList != null) { + disputeList.stream().forEach(dispute -> { + dispute.setStorage(storage); + String tradeId = dispute.getTradeId(); + if (dispute.isClosed()) { + if (closedDisputeHandler != null) { + closedDisputeHandler.accept(tradeId); + } + } + }); + } else { + log.warn("disputes is null"); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Package scope + /////////////////////////////////////////////////////////////////////////////////////////// + + void onAllServicesInitialized() { + if (disputeList != null) { + disputeList.getList().addListener((ListChangeListener) change -> { + change.next(); + onDisputesChangeListener(change.getAddedSubList(), change.getRemoved()); + }); + onDisputesChangeListener(disputeList.getList(), null); + } else { + log.warn("disputes is null"); + } + } + + String getNrOfDisputes(boolean isBuyer, Contract contract) { + return String.valueOf(getDisputesAsObservableList().stream() + .filter(e -> { + Contract contract1 = e.getContract(); + if (contract1 == null) + return false; + + if (isBuyer) { + NodeAddress buyerNodeAddress = contract1.getBuyerNodeAddress(); + return buyerNodeAddress != null && buyerNodeAddress.equals(contract.getBuyerNodeAddress()); + } else { + NodeAddress sellerNodeAddress = contract1.getSellerNodeAddress(); + return sellerNodeAddress != null && sellerNodeAddress.equals(contract.getSellerNodeAddress()); + } + }) + .collect(Collectors.toSet()).size()); + } + + ObservableList getDisputesAsObservableList() { + if (disputeList == null) { + log.warn("disputes is null"); + return FXCollections.observableArrayList(); + } + return disputeList.getList(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void onDisputesChangeListener(List addedList, + @Nullable List removedList) { + if (removedList != null) { + removedList.forEach(dispute -> { + String id = dispute.getId(); + if (disputeIsClosedSubscriptionsMap.containsKey(id)) { + disputeIsClosedSubscriptionsMap.get(id).unsubscribe(); + disputeIsClosedSubscriptionsMap.remove(id); + } + }); + } + addedList.forEach(dispute -> { + String id = dispute.getId(); + Subscription disputeStateSubscription = EasyBind.subscribe(dispute.isClosedProperty(), + isClosed -> { + if (disputeList != null) { + // We get the event before the list gets updated, so we execute on next frame + UserThread.execute(() -> { + int openDisputes = (int) disputeList.getList().stream() + .filter(e -> !e.isClosed()).count(); + numOpenDisputes.set(openDisputes); + }); + } + }); + disputeIsClosedSubscriptionsMap.put(id, disputeStateSubscription); + }); + } + + public void persist() { + if (disputeList != null) { + disputeList.persist(); + } + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java new file mode 100644 index 0000000000..7049dbcc43 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -0,0 +1,701 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOfferManager; +import bisq.core.support.SupportManager; +import bisq.core.support.dispute.messages.DisputeResultMessage; +import bisq.core.support.dispute.messages.OpenNewDisputeMessage; +import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.messages.ChatMessage; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; + +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendMailboxMessageListener; + +import bisq.common.app.Version; +import bisq.common.crypto.PubKeyRing; +import bisq.common.handlers.FaultHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.storage.Storage; +import bisq.common.util.Tuple2; + +import javafx.beans.property.IntegerProperty; + +import javafx.collections.ObservableList; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public abstract class DisputeManager> extends SupportManager { + protected final TradeWalletService tradeWalletService; + protected final BtcWalletService walletService; + protected final TradeManager tradeManager; + protected final ClosedTradableManager closedTradableManager; + protected final OpenOfferManager openOfferManager; + protected final PubKeyRing pubKeyRing; + protected final DisputeListService disputeListService; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public DisputeManager(P2PService p2PService, + TradeWalletService tradeWalletService, + BtcWalletService walletService, + WalletsSetup walletsSetup, + TradeManager tradeManager, + ClosedTradableManager closedTradableManager, + OpenOfferManager openOfferManager, + PubKeyRing pubKeyRing, + DisputeListService disputeListService) { + super(p2PService, walletsSetup); + + this.tradeWalletService = tradeWalletService; + this.walletService = walletService; + this.tradeManager = tradeManager; + this.closedTradableManager = closedTradableManager; + this.openOfferManager = openOfferManager; + this.pubKeyRing = pubKeyRing; + this.disputeListService = disputeListService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void persist() { + disputeListService.persist(); + } + + @Override + public NodeAddress getPeerNodeAddress(ChatMessage message) { + Optional disputeOptional = findDispute(message); + if (!disputeOptional.isPresent()) { + log.warn("Could not find dispute for tradeId = {} traderId = {}", + message.getTradeId(), message.getTraderId()); + return null; + } + return getNodeAddressPubKeyRingTuple(disputeOptional.get()).first; + } + + @Override + public PubKeyRing getPeerPubKeyRing(ChatMessage message) { + Optional disputeOptional = findDispute(message); + if (!disputeOptional.isPresent()) { + log.warn("Could not find dispute for tradeId = {} traderId = {}", + message.getTradeId(), message.getTraderId()); + return null; + } + + return getNodeAddressPubKeyRingTuple(disputeOptional.get()).second; + } + + @Override + public List getAllChatMessages() { + return getDisputeList().stream() + .flatMap(dispute -> dispute.getChatMessages().stream()) + .collect(Collectors.toList()); + } + + @Override + public boolean channelOpen(ChatMessage message) { + return findDispute(message).isPresent(); + } + + @Override + public void addAndPersistChatMessage(ChatMessage message) { + findDispute(message).ifPresent(dispute -> { + if (dispute.getChatMessages().stream().noneMatch(m -> m.getUid().equals(message.getUid()))) { + dispute.addAndPersistChatMessage(message); + } else { + log.warn("We got a chatMessage what we have already stored. UId = {} TradeId = {}", + message.getUid(), message.getTradeId()); + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract methods + /////////////////////////////////////////////////////////////////////////////////////////// + + // We get that message at both peers. The dispute object is in context of the trader + public abstract void onDisputeResultMessage(DisputeResultMessage disputeResultMessage); + + public abstract NodeAddress getAgentNodeAddress(Dispute dispute); + + protected abstract Trade.DisputeState getDisputeState_StartedByPeer(); + + public abstract void cleanupDisputes(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Delegates for disputeListService + /////////////////////////////////////////////////////////////////////////////////////////// + + public IntegerProperty getNumOpenDisputes() { + return disputeListService.getNumOpenDisputes(); + } + + public Storage getStorage() { + return disputeListService.getStorage(); + } + + public ObservableList getDisputesAsObservableList() { + return disputeListService.getDisputesAsObservableList(); + } + + public String getNrOfDisputes(boolean isBuyer, Contract contract) { + return disputeListService.getNrOfDisputes(isBuyer, contract); + } + + private T getDisputeList() { + return disputeListService.getDisputeList(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + super.onAllServicesInitialized(); + disputeListService.onAllServicesInitialized(); + + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + tryApplyMessages(); + } + }); + + walletsSetup.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> { + if (walletsSetup.isDownloadComplete()) + tryApplyMessages(); + }); + + walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> { + if (walletsSetup.hasSufficientPeersForBroadcast()) + tryApplyMessages(); + }); + + tryApplyMessages(); + cleanupDisputes(); + } + + public boolean isTrader(Dispute dispute) { + return pubKeyRing.equals(dispute.getTraderPubKeyRing()); + } + + + public Optional findOwnDispute(String tradeId) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return Optional.empty(); + } + return disputeList.stream().filter(e -> e.getTradeId().equals(tradeId)).findAny(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Message handler + /////////////////////////////////////////////////////////////////////////////////////////// + + // arbitrator receives that from trader who opens dispute + protected void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return; + } + + String errorMessage = null; + Dispute dispute = openNewDisputeMessage.getDispute(); + Contract contractFromOpener = dispute.getContract(); + PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getSellerPubKeyRing() : contractFromOpener.getBuyerPubKeyRing(); + if (isAgent(dispute)) { + if (!disputeList.contains(dispute)) { + Optional storedDisputeOptional = findDispute(dispute); + if (!storedDisputeOptional.isPresent()) { + dispute.setStorage(disputeListService.getStorage()); + disputeList.add(dispute); + errorMessage = sendPeerOpenedDisputeMessage(dispute, contractFromOpener, peersPubKeyRing); + } else { + // valid case if both have opened a dispute and agent was not online. + log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", + dispute.getTradeId()); + } + } else { + errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); + log.warn(errorMessage); + } + } else { + errorMessage = "Trader received openNewDisputeMessage. That must never happen."; + log.error(errorMessage); + } + + // We use the ChatMessage not the openNewDisputeMessage for the ACK + ObservableList messages = openNewDisputeMessage.getDispute().getChatMessages(); + if (!messages.isEmpty()) { + ChatMessage msg = messages.get(0); + PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getBuyerPubKeyRing() : contractFromOpener.getSellerPubKeyRing(); + sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage); + } + } + + // not dispute requester receives that from arbitrator + protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return; + } + + String errorMessage = null; + Dispute dispute = peerOpenedDisputeMessage.getDispute(); + if (!isAgent(dispute)) { + if (!disputeList.contains(dispute)) { + Optional storedDisputeOptional = findDispute(dispute); + if (!storedDisputeOptional.isPresent()) { + dispute.setStorage(disputeListService.getStorage()); + disputeList.add(dispute); + Optional tradeOptional = tradeManager.getTradeById(dispute.getTradeId()); + tradeOptional.ifPresent(trade -> trade.setDisputeState(getDisputeState_StartedByPeer())); + errorMessage = null; + } else { + // valid case if both have opened a dispute and agent was not online. + log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", + dispute.getTradeId()); + } + } else { + errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); + log.warn(errorMessage); + } + } else { + errorMessage = "Arbitrator received peerOpenedDisputeMessage. That must never happen."; + log.error(errorMessage); + } + + // We use the ChatMessage not the peerOpenedDisputeMessage for the ACK + ObservableList messages = peerOpenedDisputeMessage.getDispute().getChatMessages(); + if (!messages.isEmpty()) { + ChatMessage msg = messages.get(0); + sendAckMessage(msg, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage); + } + + sendAckMessage(peerOpenedDisputeMessage, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Send message + /////////////////////////////////////////////////////////////////////////////////////////// + + public void sendOpenNewDisputeMessage(Dispute dispute, + boolean reOpen, + ResultHandler resultHandler, + FaultHandler faultHandler) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return; + } + + if (disputeList.contains(dispute)) { + String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId(); + log.warn(msg); + faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); + return; + } + + Optional storedDisputeOptional = findDispute(dispute); + if (!storedDisputeOptional.isPresent() || reOpen) { + String disputeInfo = getDisputeInfo(dispute.isMediationDispute()); + 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(), + pubKeyRing.hashCode(), + false, + Res.get("support.systemMsg", sysMsg), + p2PService.getAddress()); + chatMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(chatMessage); + if (!reOpen) { + disputeList.add(dispute); + } + + NodeAddress agentNodeAddress = getAgentNodeAddress(dispute); + OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute, + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType()); + log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, + openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + chatMessage.getUid()); + p2PService.sendEncryptedMailboxMessage(agentNodeAddress, + dispute.getAgentPubKeyRing(), + openNewDisputeMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, + openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + chatMessage.getUid()); + + // 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); + disputeList.persist(); + resultHandler.handleResult(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}", + openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, + openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + chatMessage.getUid()); + + // 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); + disputeList.persist(); + resultHandler.handleResult(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " + + "chatMessage.uid={}, errorMessage={}", + openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress, + openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(), + chatMessage.getUid(), errorMessage); + + // 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); + disputeList.persist(); + faultHandler.handleFault("Sending dispute message failed: " + + errorMessage, new DisputeMessageDeliveryFailedException()); + } + } + ); + } else { + String msg = "We got a dispute already open for that trade and trading peer.\n" + + "TradeId = " + dispute.getTradeId(); + log.warn(msg); + faultHandler.handleFault(msg, new DisputeAlreadyOpenException()); + } + } + + // arbitrator sends that to trading peer when he received openDispute request + private String sendPeerOpenedDisputeMessage(Dispute disputeFromOpener, + Contract contractFromOpener, + PubKeyRing pubKeyRing) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return null; + } + + Dispute dispute = new Dispute(disputeListService.getStorage(), + disputeFromOpener.getTradeId(), + pubKeyRing.hashCode(), + !disputeFromOpener.isDisputeOpenerIsBuyer(), + !disputeFromOpener.isDisputeOpenerIsMaker(), + pubKeyRing, + disputeFromOpener.getTradeDate().getTime(), + contractFromOpener, + disputeFromOpener.getContractHash(), + disputeFromOpener.getDepositTxSerialized(), + disputeFromOpener.getPayoutTxSerialized(), + disputeFromOpener.getDepositTxId(), + disputeFromOpener.getPayoutTxId(), + disputeFromOpener.getContractAsJson(), + disputeFromOpener.getMakerContractSignature(), + disputeFromOpener.getTakerContractSignature(), + disputeFromOpener.getAgentPubKeyRing(), + disputeFromOpener.isSupportTicket()); + Optional storedDisputeOptional = findDispute(dispute); + if (!storedDisputeOptional.isPresent()) { + String disputeInfo = getDisputeInfo(dispute.isMediationDispute()); + String sysMsg = dispute.isSupportTicket() ? + Res.get("support.peerOpenedTicket", disputeInfo) + : Res.get("support.peerOpenedDispute", disputeInfo); + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + pubKeyRing.hashCode(), + false, + Res.get("support.systemMsg", sysMsg), + p2PService.getAddress()); + chatMessage.setSystemMessage(true); + dispute.addAndPersistChatMessage(chatMessage); + disputeList.add(dispute); + + // we mirrored dispute already! + Contract contract = dispute.getContract(); + PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing(); + NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress(); + PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute, + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType()); + log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid()); + p2PService.sendEncryptedMailboxMessage(peersNodeAddress, + peersPubKeyRing, + peerOpenedDisputeMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid()); + + // 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); + disputeList.persist(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid()); + + // 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); + disputeList.persist(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " + + "chatMessage.uid={}, errorMessage={}", + peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress, + peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(), + chatMessage.getUid(), errorMessage); + + // 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); + disputeList.persist(); + } + } + ); + return null; + } else { + // valid case if both have opened a dispute and agent was not online. + log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}", + dispute.getTradeId()); + return null; + } + } + + // arbitrator send result to trader + public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String text) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return; + } + + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + dispute.getTradeId(), + dispute.getTraderPubKeyRing().hashCode(), + false, + text, + p2PService.getAddress()); + + dispute.addAndPersistChatMessage(chatMessage); + disputeResult.setChatMessage(chatMessage); + + NodeAddress peersNodeAddress; + Contract contract = dispute.getContract(); + if (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing())) + peersNodeAddress = contract.getBuyerNodeAddress(); + else + peersNodeAddress = contract.getSellerNodeAddress(); + DisputeResultMessage disputeResultMessage = new DisputeResultMessage(disputeResult, + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType()); + log.info("Send {} to peer {}. tradeId={}, disputeResultMessage.uid={}, chatMessage.uid={}", + disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, disputeResultMessage.getTradeId(), + disputeResultMessage.getUid(), chatMessage.getUid()); + p2PService.sendEncryptedMailboxMessage(peersNodeAddress, + dispute.getTraderPubKeyRing(), + disputeResultMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, disputeResultMessage.uid={}, " + + "chatMessage.uid={}", + disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, + disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the disputeResultMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setArrived(true); + disputeList.persist(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, disputeResultMessage.uid={}, " + + "chatMessage.uid={}", + disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, + disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the disputeResultMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setStoredInMailbox(true); + disputeList.persist(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, disputeResultMessage.uid={}, " + + "chatMessage.uid={}, errorMessage={}", + disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, + disputeResultMessage.getTradeId(), disputeResultMessage.getUid(), + chatMessage.getUid(), errorMessage); + + // We use the chatMessage wrapped inside the disputeResultMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setSendMessageError(errorMessage); + disputeList.persist(); + } + } + ); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private Tuple2 getNodeAddressPubKeyRingTuple(Dispute dispute) { + PubKeyRing receiverPubKeyRing = null; + NodeAddress peerNodeAddress = null; + if (isTrader(dispute)) { + receiverPubKeyRing = dispute.getAgentPubKeyRing(); + peerNodeAddress = getAgentNodeAddress(dispute); + } else if (isAgent(dispute)) { + receiverPubKeyRing = dispute.getTraderPubKeyRing(); + Contract contract = dispute.getContract(); + if (contract.getBuyerPubKeyRing().equals(receiverPubKeyRing)) + peerNodeAddress = contract.getBuyerNodeAddress(); + else + peerNodeAddress = contract.getSellerNodeAddress(); + } else { + log.error("That must not happen. Trader cannot communicate to other trader."); + } + return new Tuple2<>(peerNodeAddress, receiverPubKeyRing); + } + + private boolean isAgent(Dispute dispute) { + return pubKeyRing.equals(dispute.getAgentPubKeyRing()); + } + + private Optional findDispute(Dispute dispute) { + return findDispute(dispute.getTradeId(), dispute.getTraderId()); + } + + protected Optional findDispute(DisputeResult disputeResult) { + ChatMessage chatMessage = disputeResult.getChatMessage(); + checkNotNull(chatMessage, "chatMessage must not be null"); + return findDispute(disputeResult.getTradeId(), disputeResult.getTraderId()); + } + + private Optional findDispute(ChatMessage message) { + return findDispute(message.getTradeId(), message.getTraderId()); + } + + private Optional findDispute(String tradeId, int traderId) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return Optional.empty(); + } + return disputeList.stream() + .filter(e -> e.getTradeId().equals(tradeId) && e.getTraderId() == traderId) + .findAny(); + } + + public Optional findDispute(String tradeId) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return Optional.empty(); + } + return disputeList.stream() + .filter(e -> e.getTradeId().equals(tradeId)) + .findAny(); + } + + private String getDisputeInfo(boolean isMediationDispute) { + String role = isMediationDispute ? Res.get("shared.mediator").toLowerCase() : + Res.get("shared.arbitrator2").toLowerCase(); + String link = isMediationDispute ? "https://docs.bisq.network/trading-rules.html#mediation" : + "https://bisq.network/docs/exchange/arbitration-system"; + return Res.get("support.initialInfo", role, role, link); + } +} diff --git a/core/src/main/java/bisq/core/arbitration/MessageDeliveryFailedException.java b/core/src/main/java/bisq/core/support/dispute/DisputeMessageDeliveryFailedException.java similarity index 81% rename from core/src/main/java/bisq/core/arbitration/MessageDeliveryFailedException.java rename to core/src/main/java/bisq/core/support/dispute/DisputeMessageDeliveryFailedException.java index 10275e9451..39eeebd65f 100644 --- a/core/src/main/java/bisq/core/arbitration/MessageDeliveryFailedException.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeMessageDeliveryFailedException.java @@ -15,10 +15,10 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration; +package bisq.core.support.dispute; -public class MessageDeliveryFailedException extends Exception { - public MessageDeliveryFailedException() { +public class DisputeMessageDeliveryFailedException extends Exception { + public DisputeMessageDeliveryFailedException() { super(); } } diff --git a/core/src/main/java/bisq/core/arbitration/DisputeResult.java b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java similarity index 91% rename from core/src/main/java/bisq/core/arbitration/DisputeResult.java rename to core/src/main/java/bisq/core/support/dispute/DisputeResult.java index b5693c79d6..db11d29ef0 100644 --- a/core/src/main/java/bisq/core/arbitration/DisputeResult.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeResult.java @@ -15,9 +15,9 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration; +package bisq.core.support.dispute; -import bisq.core.arbitration.messages.DisputeCommunicationMessage; +import bisq.core.support.messages.ChatMessage; import bisq.common.proto.ProtoUtil; import bisq.common.proto.network.NetworkPayload; @@ -74,7 +74,7 @@ public final class DisputeResult implements NetworkPayload { private final StringProperty summaryNotesProperty = new SimpleStringProperty(""); @Setter @Nullable - private DisputeCommunicationMessage disputeCommunicationMessage; + private ChatMessage chatMessage; @Setter @Nullable private byte[] arbitratorSignature; @@ -100,7 +100,7 @@ public final class DisputeResult implements NetworkPayload { boolean idVerification, boolean screenCast, String summaryNotes, - @Nullable DisputeCommunicationMessage disputeCommunicationMessage, + @Nullable ChatMessage chatMessage, @Nullable byte[] arbitratorSignature, long buyerPayoutAmount, long sellerPayoutAmount, @@ -115,7 +115,7 @@ public final class DisputeResult implements NetworkPayload { this.idVerificationProperty.set(idVerification); this.screenCastProperty.set(screenCast); this.summaryNotesProperty.set(summaryNotes); - this.disputeCommunicationMessage = disputeCommunicationMessage; + this.chatMessage = chatMessage; this.arbitratorSignature = arbitratorSignature; this.buyerPayoutAmount = buyerPayoutAmount; this.sellerPayoutAmount = sellerPayoutAmount; @@ -138,7 +138,7 @@ public final class DisputeResult implements NetworkPayload { proto.getIdVerification(), proto.getScreenCast(), proto.getSummaryNotes(), - proto.getDisputeCommunicationMessage() == null ? null : DisputeCommunicationMessage.fromPayloadProto(proto.getDisputeCommunicationMessage()), + proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()), proto.getArbitratorSignature().toByteArray(), proto.getBuyerPayoutAmount(), proto.getSellerPayoutAmount(), @@ -165,8 +165,8 @@ public final class DisputeResult implements NetworkPayload { Optional.ofNullable(arbitratorSignature).ifPresent(arbitratorSignature -> builder.setArbitratorSignature(ByteString.copyFrom(arbitratorSignature))); Optional.ofNullable(arbitratorPubKey).ifPresent(arbitratorPubKey -> builder.setArbitratorPubKey(ByteString.copyFrom(arbitratorPubKey))); Optional.ofNullable(winner).ifPresent(result -> builder.setWinner(protobuf.DisputeResult.Winner.valueOf(winner.name()))); - Optional.ofNullable(disputeCommunicationMessage).ifPresent(disputeCommunicationMessage -> - builder.setDisputeCommunicationMessage(disputeCommunicationMessage.toProtoNetworkEnvelope().getDisputeCommunicationMessage())); + Optional.ofNullable(chatMessage).ifPresent(chatMessage -> + builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage())); return builder.build(); } @@ -242,7 +242,7 @@ public final class DisputeResult implements NetworkPayload { ",\n idVerificationProperty=" + idVerificationProperty + ",\n screenCastProperty=" + screenCastProperty + ",\n summaryNotesProperty=" + summaryNotesProperty + - ",\n disputeCommunicationMessage=" + disputeCommunicationMessage + + ",\n chatMessage=" + chatMessage + ",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) + ",\n buyerPayoutAmount=" + buyerPayoutAmount + ",\n sellerPayoutAmount=" + sellerPayoutAmount + diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeSession.java b/core/src/main/java/bisq/core/support/dispute/DisputeSession.java new file mode 100644 index 0000000000..cebe715e16 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/DisputeSession.java @@ -0,0 +1,79 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute; + +import bisq.core.support.SupportSession; +import bisq.core.support.messages.ChatMessage; + +import bisq.common.crypto.PubKeyRing; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public abstract class DisputeSession extends SupportSession { + @Nullable + private Dispute dispute; + private final boolean isTrader; + + public DisputeSession(@Nullable Dispute dispute, boolean isTrader) { + super(); + this.dispute = dispute; + this.isTrader = isTrader; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Dependent on selected dispute + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public boolean isClient() { + return isTrader; + } + + @Override + public String getTradeId() { + return dispute != null ? dispute.getTradeId() : ""; + } + + @Override + public PubKeyRing getClientPubKeyRing() { + // Get pubKeyRing of trader. Arbitrator is considered server for the chat session + return dispute != null ? dispute.getTraderPubKeyRing() : null; + } + + @Override + public ObservableList getObservableChatMessageList() { + return dispute != null ? dispute.getChatMessages() : FXCollections.observableArrayList(); + } + + @Override + public boolean chatIsOpen() { + return dispute != null && !dispute.isClosed(); + } + + @Override + public boolean isDisputeAgent() { + return !isClient(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgent.java b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgent.java new file mode 100644 index 0000000000..aa583d04de --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgent.java @@ -0,0 +1,113 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.agent; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.storage.payload.ExpirablePayload; +import bisq.network.p2p.storage.payload.ProtectedStoragePayload; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.util.ExtraDataMapValidator; +import bisq.common.util.Utilities; + +import java.security.PublicKey; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@EqualsAndHashCode +@Slf4j +@Getter +public abstract class DisputeAgent implements ProtectedStoragePayload, ExpirablePayload { + public static final long TTL = TimeUnit.DAYS.toMillis(10); + + protected final NodeAddress nodeAddress; + protected final PubKeyRing pubKeyRing; + protected final List languageCodes; + protected final long registrationDate; + protected final byte[] registrationPubKey; + protected final String registrationSignature; + @Nullable + protected final String emailAddress; + @Nullable + protected final String info; + + // Should be only used in emergency case if we need to add data but do not want to break backward compatibility + // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new + // field in a class would break that hash and therefore break the storage mechanism. + @Nullable + protected Map extraDataMap; + + public DisputeAgent(NodeAddress nodeAddress, + PubKeyRing pubKeyRing, + List languageCodes, + long registrationDate, + byte[] registrationPubKey, + String registrationSignature, + @Nullable String emailAddress, + @Nullable String info, + @Nullable Map extraDataMap) { + this.nodeAddress = nodeAddress; + this.pubKeyRing = pubKeyRing; + this.languageCodes = languageCodes; + this.registrationDate = registrationDate; + this.registrationPubKey = registrationPubKey; + this.registrationSignature = registrationSignature; + this.emailAddress = emailAddress; + this.info = info; + this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public long getTTL() { + return TTL; + } + + @Override + public PublicKey getOwnerPubKey() { + return pubKeyRing.getSignaturePubKey(); + } + + + @Override + public String toString() { + return "DisputeAgent{" + + "\n nodeAddress=" + nodeAddress + + ",\n pubKeyRing=" + pubKeyRing + + ",\n languageCodes=" + languageCodes + + ",\n registrationDate=" + registrationDate + + ",\n registrationPubKey=" + Utilities.bytesAsHexString(registrationPubKey) + + ",\n registrationSignature='" + registrationSignature + '\'' + + ",\n emailAddress='" + emailAddress + '\'' + + ",\n info='" + info + '\'' + + ",\n extraDataMap=" + extraDataMap + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentManager.java b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentManager.java new file mode 100644 index 0000000000..16cc1b38e8 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentManager.java @@ -0,0 +1,338 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.agent; + +import bisq.core.filter.FilterManager; +import bisq.core.user.User; + +import bisq.network.p2p.BootstrapListener; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.HashMapChangedListener; +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.crypto.KeyRing; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.Utils; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableMap; + +import java.security.PublicKey; +import java.security.SignatureException; + +import java.math.BigInteger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static org.bitcoinj.core.Utils.HEX; + +@Slf4j +public abstract class DisputeAgentManager { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + protected static final long REPUBLISH_MILLIS = DisputeAgent.TTL / 2; + protected static final long RETRY_REPUBLISH_SEC = 5; + protected static final long REPEATED_REPUBLISH_AT_STARTUP_SEC = 60; + + protected final List publicKeys; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Instance fields + /////////////////////////////////////////////////////////////////////////////////////////// + + protected final KeyRing keyRing; + protected final DisputeAgentService disputeAgentService; + protected final User user; + protected final FilterManager filterManager; + protected final ObservableMap observableMap = FXCollections.observableHashMap(); + protected List persistedAcceptedDisputeAgents; + protected Timer republishTimer, retryRepublishTimer; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public DisputeAgentManager(KeyRing keyRing, + DisputeAgentService disputeAgentService, + User user, + FilterManager filterManager, + boolean useDevPrivilegeKeys) { + this.keyRing = keyRing; + this.disputeAgentService = disputeAgentService; + this.user = user; + this.filterManager = filterManager; + publicKeys = useDevPrivilegeKeys ? Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) : getPubKeyList(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Abstract methods + /////////////////////////////////////////////////////////////////////////////////////////// + + protected abstract List getPubKeyList(); + + protected abstract boolean isExpectedInstance(ProtectedStorageEntry data); + + protected abstract void addAcceptedDisputeAgentToUser(T disputeAgent); + + protected abstract T getRegisteredDisputeAgentFromUser(); + + protected abstract void clearAcceptedDisputeAgentsAtUser(); + + protected abstract List getAcceptedDisputeAgentsFromUser(); + + protected abstract void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data); + + protected abstract void setRegisteredDisputeAgentAtUser(T disputeAgent); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void onAllServicesInitialized() { + disputeAgentService.addHashSetChangedListener(new HashMapChangedListener() { + @Override + public void onAdded(ProtectedStorageEntry data) { + if (isExpectedInstance(data)) { + updateMap(); + } + } + + @Override + public void onRemoved(ProtectedStorageEntry data) { + if (isExpectedInstance(data)) { + updateMap(); + removeAcceptedDisputeAgentFromUser(data); + } + } + }); + + persistedAcceptedDisputeAgents = new ArrayList<>(getAcceptedDisputeAgentsFromUser()); + clearAcceptedDisputeAgentsAtUser(); + + if (getRegisteredDisputeAgentFromUser() != null) { + P2PService p2PService = disputeAgentService.getP2PService(); + if (p2PService.isBootstrapped()) + startRepublishDisputeAgent(); + else + p2PService.addP2PServiceListener(new BootstrapListener() { + @Override + public void onUpdatedDataReceived() { + startRepublishDisputeAgent(); + } + }); + } + + filterManager.filterProperty().addListener((observable, oldValue, newValue) -> updateMap()); + + updateMap(); + } + + public void shutDown() { + stopRepublishTimer(); + stopRetryRepublishTimer(); + } + + protected void startRepublishDisputeAgent() { + if (republishTimer == null) { + republishTimer = UserThread.runPeriodically(this::republish, REPUBLISH_MILLIS, TimeUnit.MILLISECONDS); + UserThread.runAfter(this::republish, REPEATED_REPUBLISH_AT_STARTUP_SEC); + republish(); + } + } + + public void updateMap() { + Map map = disputeAgentService.getDisputeAgents(); + observableMap.clear(); + Map filtered = map.values().stream() + .filter(e -> { + String pubKeyAsHex = Utils.HEX.encode(e.getRegistrationPubKey()); + boolean isInPublicKeyInList = isPublicKeyInList(pubKeyAsHex); + if (!isInPublicKeyInList) { + if (DevEnv.DEV_PRIVILEGE_PUB_KEY.equals(pubKeyAsHex)) + log.info("We got the DEV_PRIVILEGE_PUB_KEY in our list of publicKeys. RegistrationPubKey={}, nodeAddress={}", + Utilities.bytesAsHexString(e.getRegistrationPubKey()), + e.getNodeAddress().getFullAddress()); + else + log.warn("We got an disputeAgent which is not in our list of publicKeys. RegistrationPubKey={}, nodeAddress={}", + Utilities.bytesAsHexString(e.getRegistrationPubKey()), + e.getNodeAddress().getFullAddress()); + } + final boolean isSigValid = verifySignature(e.getPubKeyRing().getSignaturePubKey(), + e.getRegistrationPubKey(), + e.getRegistrationSignature()); + if (!isSigValid) + log.warn("Sig check for disputeAgent failed. DisputeAgent={}", e.toString()); + + return isInPublicKeyInList && isSigValid; + }) + .collect(Collectors.toMap(DisputeAgent::getNodeAddress, Function.identity())); + + observableMap.putAll(filtered); + observableMap.values().forEach(this::addAcceptedDisputeAgentToUser); + + log.info("Available disputeAgents: {}", observableMap.keySet()); + } + + + public void addDisputeAgent(T disputeAgent, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + setRegisteredDisputeAgentAtUser(disputeAgent); + observableMap.put(disputeAgent.getNodeAddress(), disputeAgent); + disputeAgentService.addDisputeAgent(disputeAgent, + () -> { + log.info("DisputeAgent successfully saved in P2P network"); + resultHandler.handleResult(); + + if (observableMap.size() > 0) + UserThread.runAfter(this::updateMap, 100, TimeUnit.MILLISECONDS); + }, + errorMessageHandler); + } + + + public void removeDisputeAgent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + T registeredDisputeAgent = getRegisteredDisputeAgentFromUser(); + if (registeredDisputeAgent != null) { + setRegisteredDisputeAgentAtUser(null); + observableMap.remove(registeredDisputeAgent.getNodeAddress()); + disputeAgentService.removeDisputeAgent(registeredDisputeAgent, + () -> { + log.debug("DisputeAgent successfully removed from P2P network"); + resultHandler.handleResult(); + }, + errorMessageHandler); + } + } + + public ObservableMap getObservableMap() { + return observableMap; + } + + // A protected key is handed over to selected disputeAgents for registration. + // An invited disputeAgent will sign at registration his storageSignaturePubKey with that protected key and attach the signature and pubKey to his data. + // Other users will check the signature with the list of public keys hardcoded in the app. + public String signStorageSignaturePubKey(ECKey key) { + String keyToSignAsHex = Utils.HEX.encode(keyRing.getPubKeyRing().getSignaturePubKey().getEncoded()); + return key.signMessage(keyToSignAsHex); + } + + @Nullable + public ECKey getRegistrationKey(String privKeyBigIntString) { + try { + return ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyBigIntString))); + } catch (Throwable t) { + return null; + } + } + + public boolean isPublicKeyInList(String pubKeyAsHex) { + return publicKeys.contains(pubKeyAsHex); + } + + public boolean isAgentAvailableForLanguage(String languageCode) { + return observableMap.values().stream().anyMatch(agent -> + agent.getLanguageCodes().stream().anyMatch(lc -> lc.equals(languageCode))); + } + + public List getDisputeAgentLanguages(List nodeAddresses) { + return observableMap.values().stream() + .filter(disputeAgent -> nodeAddresses.stream().anyMatch(nodeAddress -> nodeAddress.equals(disputeAgent.getNodeAddress()))) + .flatMap(disputeAgent -> disputeAgent.getLanguageCodes().stream()) + .distinct() + .collect(Collectors.toList()); + } + + public Optional getDisputeAgentByNodeAddress(NodeAddress nodeAddress) { + return observableMap.containsKey(nodeAddress) ? + Optional.of(observableMap.get(nodeAddress)) : + Optional.empty(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // protected + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void republish() { + T registeredDisputeAgent = getRegisteredDisputeAgentFromUser(); + if (registeredDisputeAgent != null) { + addDisputeAgent(registeredDisputeAgent, + this::updateMap, + errorMessage -> { + if (retryRepublishTimer == null) + retryRepublishTimer = UserThread.runPeriodically(() -> { + stopRetryRepublishTimer(); + republish(); + }, RETRY_REPUBLISH_SEC); + } + ); + } + } + + protected boolean verifySignature(PublicKey storageSignaturePubKey, byte[] registrationPubKey, String signature) { + String keyToSignAsHex = Utils.HEX.encode(storageSignaturePubKey.getEncoded()); + try { + ECKey key = ECKey.fromPublicOnly(registrationPubKey); + key.verifyMessage(keyToSignAsHex, signature); + return true; + } catch (SignatureException e) { + log.warn("verifySignature failed"); + return false; + } + } + + + protected void stopRetryRepublishTimer() { + if (retryRepublishTimer != null) { + retryRepublishTimer.stop(); + retryRepublishTimer = null; + } + } + + protected void stopRepublishTimer() { + if (republishTimer != null) { + republishTimer.stop(); + republishTimer = null; + } + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentService.java b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentService.java new file mode 100644 index 0000000000..193ba9a768 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/agent/DisputeAgentService.java @@ -0,0 +1,121 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.agent; + +import bisq.core.app.BisqEnvironment; +import bisq.core.filter.FilterManager; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.storage.HashMapChangedListener; + +import bisq.common.app.DevEnv; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.util.Utilities; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import lombok.extern.slf4j.Slf4j; + +/** + * Used to store disputeAgents profile and load map of disputeAgents + */ +@Slf4j +public abstract class DisputeAgentService { + protected final P2PService p2PService; + protected final FilterManager filterManager; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public DisputeAgentService(P2PService p2PService, FilterManager filterManager) { + this.p2PService = p2PService; + this.filterManager = filterManager; + } + + public void addHashSetChangedListener(HashMapChangedListener hashMapChangedListener) { + p2PService.addHashSetChangedListener(hashMapChangedListener); + } + + public void addDisputeAgent(T disputeAgent, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + log.debug("addDisputeAgent disputeAgent.hashCode() " + disputeAgent.hashCode()); + if (!BisqEnvironment.getBaseCurrencyNetwork().isMainnet() || + !Utilities.encodeToHex(disputeAgent.getRegistrationPubKey()).equals(DevEnv.DEV_PRIVILEGE_PUB_KEY)) { + boolean result = p2PService.addProtectedStorageEntry(disputeAgent, true); + if (result) { + log.trace("Add disputeAgent to network was successful. DisputeAgent.hashCode() = " + disputeAgent.hashCode()); + resultHandler.handleResult(); + } else { + errorMessageHandler.handleErrorMessage("Add disputeAgent failed"); + } + } else { + log.error("Attempt to publish dev disputeAgent on mainnet."); + errorMessageHandler.handleErrorMessage("Add disputeAgent failed. Attempt to publish dev disputeAgent on mainnet."); + } + } + + public void removeDisputeAgent(T disputeAgent, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + log.debug("removeDisputeAgent disputeAgent.hashCode() " + disputeAgent.hashCode()); + if (p2PService.removeData(disputeAgent, true)) { + log.trace("Remove disputeAgent from network was successful. DisputeAgent.hashCode() = " + disputeAgent.hashCode()); + resultHandler.handleResult(); + } else { + errorMessageHandler.handleErrorMessage("Remove disputeAgent failed"); + } + } + + public P2PService getP2PService() { + return p2PService; + } + + public Map getDisputeAgents() { + final List bannedDisputeAgents; + if (filterManager.getFilter() != null) { + bannedDisputeAgents = getDisputeAgentsFromFilter(); + } else { + bannedDisputeAgents = null; + } + if (bannedDisputeAgents != null) + log.warn("bannedDisputeAgents=" + bannedDisputeAgents); + Set disputeAgentSet = getDisputeAgentSet(bannedDisputeAgents); + + Map map = new HashMap<>(); + for (T disputeAgent : disputeAgentSet) { + NodeAddress disputeAgentNodeAddress = disputeAgent.getNodeAddress(); + if (!map.containsKey(disputeAgentNodeAddress)) + map.put(disputeAgentNodeAddress, disputeAgent); + else + log.warn("disputeAgentAddress already exist in disputeAgent map. Seems an disputeAgent object is already registered with the same address."); + } + return map; + } + + protected abstract Set getDisputeAgentSet(List bannedDisputeAgents); + + protected abstract List getDisputeAgentsFromFilter(); +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeList.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeList.java new file mode 100644 index 0000000000..5ce8f6b2c7 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeList.java @@ -0,0 +1,83 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.arbitration; + +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; + +import bisq.common.proto.ProtoUtil; +import bisq.common.storage.Storage; + +import com.google.protobuf.Message; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@ToString +/* + * Holds a List of arbitration dispute objects. + * + * Calls to the List are delegated because this class intercepts the add/remove calls so changes + * can be saved to disc. + */ +public final class ArbitrationDisputeList extends DisputeList { + + ArbitrationDisputeList(Storage storage) { + super(storage); + } + + @Override + public void readPersisted() { + // We need to use DisputeList as file name to not lose existing disputes which are stored in the DisputeList file + ArbitrationDisputeList persisted = storage.initAndGetPersisted(this, "DisputeList", 50); + if (persisted != null) { + list.addAll(persisted.getList()); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private ArbitrationDisputeList(Storage storage, List list) { + super(storage, list); + } + + @Override + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder().setArbitrationDisputeList(protobuf.ArbitrationDisputeList.newBuilder() + .addAllDispute(ProtoUtil.collectionToProto(new ArrayList<>(list)))).build(); + } + + public static ArbitrationDisputeList fromProto(protobuf.ArbitrationDisputeList proto, + CoreProtoResolver coreProtoResolver, + Storage storage) { + List list = proto.getDisputeList().stream() + .map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver)) + .collect(Collectors.toList()); + list.forEach(e -> e.setStorage(storage)); + return new ArbitrationDisputeList(storage, list); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeListService.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeListService.java new file mode 100644 index 0000000000..3d456f686e --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationDisputeListService.java @@ -0,0 +1,48 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.arbitration; + +import bisq.core.support.dispute.DisputeListService; + +import bisq.common.storage.Storage; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public final class ArbitrationDisputeListService extends DisputeListService { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ArbitrationDisputeListService(Storage storage) { + super(storage); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected ArbitrationDisputeList getConcreteDisputeList() { + return new ArbitrationDisputeList(storage); + } +} 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 new file mode 100644 index 0000000000..cbf859e2a2 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java @@ -0,0 +1,390 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.arbitration; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.arbitration.messages.PeerPublishedDisputePayoutTxMessage; +import bisq.core.support.dispute.messages.DisputeResultMessage; +import bisq.core.support.dispute.messages.OpenNewDisputeMessage; +import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.messages.ChatMessage; +import bisq.core.support.messages.SupportMessage; +import bisq.core.trade.Contract; +import bisq.core.trade.Tradable; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; + +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendMailboxMessageListener; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.crypto.PubKeyRing; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import java.util.Arrays; +import java.util.Optional; +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +@Singleton +public final class ArbitrationManager extends DisputeManager { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ArbitrationManager(P2PService p2PService, + TradeWalletService tradeWalletService, + BtcWalletService walletService, + WalletsSetup walletsSetup, + TradeManager tradeManager, + ClosedTradableManager closedTradableManager, + OpenOfferManager openOfferManager, + PubKeyRing pubKeyRing, + ArbitrationDisputeListService arbitrationDisputeListService) { + super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, + openOfferManager, pubKeyRing, arbitrationDisputeListService); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public SupportType getSupportType() { + return SupportType.ARBITRATION; + } + + @Override + public void dispatchMessage(SupportMessage message) { + if (canProcessMessage(message)) { + log.info("Received {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); + + if (message instanceof OpenNewDisputeMessage) { + onOpenNewDisputeMessage((OpenNewDisputeMessage) message); + } else if (message instanceof PeerOpenedDisputeMessage) { + onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); + } else if (message instanceof ChatMessage) { + onChatMessage((ChatMessage) message); + } else if (message instanceof DisputeResultMessage) { + onDisputeResultMessage((DisputeResultMessage) message); + } else if (message instanceof PeerPublishedDisputePayoutTxMessage) { + onDisputedPayoutTxMessage((PeerPublishedDisputePayoutTxMessage) message); + } else { + log.warn("Unsupported message at dispatchMessage. message={}", message); + } + } + } + + @Override + public NodeAddress getAgentNodeAddress(Dispute dispute) { + return dispute.getContract().getArbitratorNodeAddress(); + } + + @Override + protected Trade.DisputeState getDisputeState_StartedByPeer() { + return Trade.DisputeState.DISPUTE_STARTED_BY_PEER; + } + + @Override + protected AckMessageSourceType getAckMessageSourceType() { + return AckMessageSourceType.ARBITRATION_MESSAGE; + } + + @Override + public void cleanupDisputes() { + disputeListService.cleanupDisputes(tradeId -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Message handler + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + // We get that message at both peers. The dispute object is in context of the trader + public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { + DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); + ChatMessage chatMessage = disputeResult.getChatMessage(); + checkNotNull(chatMessage, "chatMessage must not be null"); + if (Arrays.equals(disputeResult.getArbitratorPubKey(), + walletService.getArbitratorAddressEntry().getPubKey())) { + log.error("Arbitrator received disputeResultMessage. That must never happen."); + return; + } + + String tradeId = disputeResult.getTradeId(); + Optional disputeOptional = findDispute(disputeResult); + String uid = disputeResultMessage.getUid(); + if (!disputeOptional.isPresent()) { + log.warn("We got a dispute result msg but we don't have a matching dispute. " + + "That might happen when we get the disputeResultMessage before the dispute was created. " + + "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); + if (!delayMsgMap.containsKey(uid)) { + // We delay 2 sec. to be sure the comm. msg gets added first + Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2); + delayMsgMap.put(uid, timer); + } else { + log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + + "That should never happen. TradeId = " + tradeId); + } + return; + } + + Dispute dispute = disputeOptional.get(); + cleanupRetryMap(uid); + if (!dispute.getChatMessages().contains(chatMessage)) { + dispute.addAndPersistChatMessage(chatMessage); + } else { + log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId()); + } + dispute.setIsClosed(true); + + if (dispute.disputeResultProperty().get() != null) { + log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " + + "again because the first close did not succeed. TradeId = " + tradeId); + } + + dispute.setDisputeResult(disputeResult); + Optional tradeOptional = tradeManager.getTradeById(tradeId); + String errorMessage = null; + boolean success = false; + try { + // We need to avoid publishing the tx from both traders as it would create problems with zero confirmation withdrawals + // There would be different transactions if both sign and publish (signers: once buyer+arb, once seller+arb) + // The tx publisher is the winner or in case both get 50% the buyer, as the buyer has more inventive to publish the tx as he receives + // more BTC as he has deposited + Contract contract = dispute.getContract(); + + boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing()); + DisputeResult.Winner publisher = disputeResult.getWinner(); + + // Sometimes the user who receives the trade amount is never online, so we might want to + // let the loser publish the tx. When the winner comes online he gets his funds as it was published by the other peer. + // Default isLoserPublisher is set to false + if (disputeResult.isLoserPublisher()) { + // we invert the logic + if (publisher == DisputeResult.Winner.BUYER) + publisher = DisputeResult.Winner.SELLER; + else if (publisher == DisputeResult.Winner.SELLER) + publisher = DisputeResult.Winner.BUYER; + } + + if ((isBuyer && publisher == DisputeResult.Winner.BUYER) + || (!isBuyer && publisher == DisputeResult.Winner.SELLER)) { + + Transaction payoutTx = null; + if (tradeOptional.isPresent()) { + payoutTx = tradeOptional.get().getPayoutTx(); + } else { + Optional tradableOptional = closedTradableManager.getTradableById(tradeId); + if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) { + payoutTx = ((Trade) tradableOptional.get()).getPayoutTx(); + } + } + + if (payoutTx == null) { + if (dispute.getDepositTxSerialized() != null) { + byte[] multiSigPubKey = isBuyer ? contract.getBuyerMultiSigPubKey() : contract.getSellerMultiSigPubKey(); + DeterministicKey multiSigKeyPair = walletService.getMultiSigKeyPair(tradeId, multiSigPubKey); + Transaction signedDisputedPayoutTx = tradeWalletService.traderSignAndFinalizeDisputedPayoutTx( + dispute.getDepositTxSerialized(), + disputeResult.getArbitratorSignature(), + disputeResult.getBuyerPayoutAmount(), + disputeResult.getSellerPayoutAmount(), + contract.getBuyerPayoutAddressString(), + contract.getSellerPayoutAddressString(), + multiSigKeyPair, + contract.getBuyerMultiSigPubKey(), + contract.getSellerMultiSigPubKey(), + disputeResult.getArbitratorPubKey() + ); + Transaction committedDisputedPayoutTx = tradeWalletService.addTxToWallet(signedDisputedPayoutTx); + tradeWalletService.broadcastTx(committedDisputedPayoutTx, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + // after successful publish we send peer the tx + dispute.setDisputePayoutTxId(transaction.getHashAsString()); + sendPeerPublishedPayoutTxMessage(transaction, dispute, contract); + updateTradeOrOpenOfferManager(tradeId); + } + + @Override + public void onFailure(TxBroadcastException exception) { + log.error(exception.getMessage()); + } + }, 15); + + success = true; + } else { + errorMessage = "DepositTx is null. TradeId = " + tradeId; + log.warn(errorMessage); + success = false; + } + } else { + log.warn("We got already a payout tx. That might be the case if the other peer did not get the " + + "payout tx and opened a dispute. TradeId = " + tradeId); + dispute.setDisputePayoutTxId(payoutTx.getHashAsString()); + sendPeerPublishedPayoutTxMessage(payoutTx, dispute, contract); + + success = true; + } + } else { + log.trace("We don't publish the tx as we are not the winning party."); + // Clean up tangling trades + if (dispute.disputeResultProperty().get() != null && dispute.isClosed()) { + updateTradeOrOpenOfferManager(tradeId); + } + + success = true; + } + } catch (TransactionVerificationException e) { + errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString(); + log.error(errorMessage, e); + success = false; + + // We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used + // we get a TransactionVerificationException. No reason to keep that dispute open... + updateTradeOrOpenOfferManager(tradeId); + + throw new RuntimeException(errorMessage); + } catch (AddressFormatException | WalletException e) { + errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString(); + log.error(errorMessage, e); + success = false; + throw new RuntimeException(errorMessage); + } finally { + // We use the chatMessage as we only persist those not the disputeResultMessage. + // If we would use the disputeResultMessage we could not lookup for the msg when we receive the AckMessage. + sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), success, errorMessage); + } + } + + // Losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer + private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) { + String uid = peerPublishedDisputePayoutTxMessage.getUid(); + String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId(); + Optional disputeOptional = findOwnDispute(tradeId); + if (!disputeOptional.isPresent()) { + log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId); + if (!delayMsgMap.containsKey(uid)) { + // We delay 3 sec. to be sure the close msg gets added first + Timer timer = UserThread.runAfter(() -> onDisputedPayoutTxMessage(peerPublishedDisputePayoutTxMessage), 3); + delayMsgMap.put(uid, timer); + } else { + log.warn("We got a peerPublishedPayoutTxMessage after we already repeated to apply the message after a delay. " + + "That should never happen. TradeId = " + tradeId); + } + return; + } + + Dispute dispute = disputeOptional.get(); + Contract contract = dispute.getContract(); + boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing()); + PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); + + cleanupRetryMap(uid); + Transaction walletTx = tradeWalletService.addTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction()); + dispute.setDisputePayoutTxId(walletTx.getHashAsString()); + BtcWalletService.printTx("Disputed payoutTx received from peer", walletTx); + + // We can only send the ack msg if we have the peersPubKeyRing which requires the dispute + sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Send messages + /////////////////////////////////////////////////////////////////////////////////////////// + + // winner (or buyer in case of 50/50) sends tx to other peer + private void sendPeerPublishedPayoutTxMessage(Transaction transaction, Dispute dispute, Contract contract) { + PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing(); + NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress(); + log.trace("sendPeerPublishedPayoutTxMessage to peerAddress " + peersNodeAddress); + PeerPublishedDisputePayoutTxMessage message = new PeerPublishedDisputePayoutTxMessage(transaction.bitcoinSerialize(), + dispute.getTradeId(), + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType()); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + p2PService.sendEncryptedMailboxMessage(peersNodeAddress, + peersPubKeyRing, + message, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + } + } + ); + } + + private void updateTradeOrOpenOfferManager(String tradeId) { + // set state after payout as we call swapTradeEntryToAvailableEntry + if (tradeManager.getTradeById(tradeId).isPresent()) { + tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED); + } else { + Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + } + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationSession.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationSession.java new file mode 100644 index 0000000000..1dc5d7a830 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationSession.java @@ -0,0 +1,33 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.arbitration; + +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeSession; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class ArbitrationSession extends DisputeSession { + + public ArbitrationSession(@Nullable Dispute dispute, boolean isTrader) { + super(dispute, isTrader); + } +} diff --git a/core/src/main/java/bisq/core/arbitration/BuyerDataItem.java b/core/src/main/java/bisq/core/support/dispute/arbitration/BuyerDataItem.java similarity index 94% rename from core/src/main/java/bisq/core/arbitration/BuyerDataItem.java rename to core/src/main/java/bisq/core/support/dispute/arbitration/BuyerDataItem.java index 5836c08961..7962a4e074 100644 --- a/core/src/main/java/bisq/core/arbitration/BuyerDataItem.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/BuyerDataItem.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration; +package bisq.core.support.dispute.arbitration; import bisq.core.account.witness.AccountAgeWitness; import bisq.core.payment.payload.PaymentAccountPayload; @@ -27,6 +27,7 @@ import java.security.PublicKey; import lombok.EqualsAndHashCode; import lombok.Getter; +// TODO consider to move to signed witness domain @Getter @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class BuyerDataItem { diff --git a/core/src/main/java/bisq/core/arbitration/Arbitrator.java b/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/Arbitrator.java similarity index 59% rename from core/src/main/java/bisq/core/arbitration/Arbitrator.java rename to core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/Arbitrator.java index f0552c08d9..12c5cb91f7 100644 --- a/core/src/main/java/bisq/core/arbitration/Arbitrator.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/Arbitrator.java @@ -15,28 +15,24 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration; +package bisq.core.support.dispute.arbitration.arbitrator; + +import bisq.core.support.dispute.agent.DisputeAgent; import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.storage.payload.ExpirablePayload; -import bisq.network.p2p.storage.payload.ProtectedStoragePayload; import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; -import bisq.common.util.ExtraDataMapValidator; import bisq.common.util.Utilities; import com.google.protobuf.ByteString; import org.springframework.util.CollectionUtils; -import java.security.PublicKey; - +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -44,30 +40,12 @@ import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; -@EqualsAndHashCode +@EqualsAndHashCode(callSuper = true) @Slf4j @Getter -public final class Arbitrator implements ProtectedStoragePayload, ExpirablePayload { - public static final long TTL = TimeUnit.DAYS.toMillis(10); - - private final NodeAddress nodeAddress; +public final class Arbitrator extends DisputeAgent { private final byte[] btcPubKey; private final String btcAddress; - private final PubKeyRing pubKeyRing; - private final List languageCodes; - private final long registrationDate; - private final byte[] registrationPubKey; - private final String registrationSignature; - @Nullable - private final String emailAddress; - @Nullable - private final String info; - - // Should be only used in emergency case if we need to add data but do not want to break backward compatibility - // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new - // field in a class would break that hash and therefore break the storage mechanism. - @Nullable - private Map extraDataMap; public Arbitrator(NodeAddress nodeAddress, byte[] btcPubKey, @@ -80,17 +58,19 @@ public final class Arbitrator implements ProtectedStoragePayload, ExpirablePaylo @Nullable String emailAddress, @Nullable String info, @Nullable Map extraDataMap) { - this.nodeAddress = nodeAddress; + + super(nodeAddress, + pubKeyRing, + languageCodes, + registrationDate, + registrationPubKey, + registrationSignature, + emailAddress, + info, + extraDataMap); + this.btcPubKey = btcPubKey; this.btcAddress = btcAddress; - this.pubKeyRing = pubKeyRing; - this.languageCodes = languageCodes; - this.registrationDate = registrationDate; - this.registrationPubKey = registrationPubKey; - this.registrationSignature = registrationSignature; - this.emailAddress = emailAddress; - this.info = info; - this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -99,7 +79,7 @@ public final class Arbitrator implements ProtectedStoragePayload, ExpirablePaylo @Override public protobuf.StoragePayload toProtoMessage() { - final protobuf.Arbitrator.Builder builder = protobuf.Arbitrator.newBuilder() + protobuf.Arbitrator.Builder builder = protobuf.Arbitrator.newBuilder() .setNodeAddress(nodeAddress.toProtoMessage()) .setBtcPubKey(ByteString.copyFrom(btcPubKey)) .setBtcAddress(btcAddress) @@ -119,7 +99,7 @@ public final class Arbitrator implements ProtectedStoragePayload, ExpirablePaylo proto.getBtcPubKey().toByteArray(), proto.getBtcAddress(), PubKeyRing.fromProto(proto.getPubKeyRing()), - proto.getLanguageCodesList().stream().collect(Collectors.toList()), + new ArrayList<>(proto.getLanguageCodesList()), proto.getRegistrationDate(), proto.getRegistrationPubKey().toByteArray(), proto.getRegistrationSignature(), @@ -133,31 +113,11 @@ public final class Arbitrator implements ProtectedStoragePayload, ExpirablePaylo // API /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public long getTTL() { - return TTL; - } - - @Override - public PublicKey getOwnerPubKey() { - return pubKeyRing.getSignaturePubKey(); - } - - @Override public String toString() { return "Arbitrator{" + - "\n nodeAddress=" + nodeAddress + - ",\n btcPubKey=" + Utilities.bytesAsHexString(btcPubKey) + + "\n btcPubKey=" + Utilities.bytesAsHexString(btcPubKey) + ",\n btcAddress='" + btcAddress + '\'' + - ",\n pubKeyRing=" + pubKeyRing + - ",\n languageCodes=" + languageCodes + - ",\n registrationDate=" + registrationDate + - ",\n registrationPubKey=" + Utilities.bytesAsHexString(registrationPubKey) + - ",\n registrationSignature='" + registrationSignature + '\'' + - ",\n emailAddress='" + emailAddress + '\'' + - ",\n info='" + info + '\'' + - ",\n extraDataMap=" + extraDataMap + - "\n}"; + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java new file mode 100644 index 0000000000..196347a66a --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorManager.java @@ -0,0 +1,103 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.arbitration.arbitrator; + +import bisq.core.app.AppOptionKeys; +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.agent.DisputeAgentManager; +import bisq.core.user.User; + +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class ArbitratorManager extends DisputeAgentManager { + + @Inject + public ArbitratorManager(KeyRing keyRing, + ArbitratorService arbitratorService, + User user, + FilterManager filterManager, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(keyRing, arbitratorService, user, filterManager, useDevPrivilegeKeys); + } + + @Override + protected List getPubKeyList() { + return List.of("0365c6af94681dbee69de1851f98d4684063bf5c2d64b1c73ed5d90434f375a054", + "031c502a60f9dbdb5ae5e438a79819e4e1f417211dd537ac12c9bc23246534c4bd", + "02c1e5a242387b6d5319ce27246cea6edaaf51c3550591b528d2578a4753c56c2c", + "025c319faf7067d9299590dd6c97fe7e56cd4dac61205ccee1cd1fc390142390a2", + "038f6e24c2bfe5d51d0a290f20a9a657c270b94ef2b9c12cd15ca3725fa798fc55", + "0255256ff7fb615278c4544a9bbd3f5298b903b8a011cd7889be19b6b1c45cbefe", + "024a3a37289f08c910fbd925ebc72b946f33feaeff451a4738ee82037b4cda2e95", + "02a88b75e9f0f8afba1467ab26799dcc38fd7a6468fb2795444b425eb43e2c10bd", + "02349a51512c1c04c67118386f4d27d768c5195a83247c150a4b722d161722ba81", + "03f718a2e0dc672c7cdec0113e72c3322efc70412bb95870750d25c32cd98de17d", + "028ff47ee2c56e66313928975c58fa4f1b19a0f81f3a96c4e9c9c3c6768075509e", + "02b517c0cbc3a49548f448ddf004ed695c5a1c52ec110be1bfd65fa0ca0761c94b", + "03df837a3a0f3d858e82f3356b71d1285327f101f7c10b404abed2abc1c94e7169", + "0203a90fb2ab698e524a5286f317a183a84327b8f8c3f7fa4a98fec9e1cefd6b72", + "023c99cc073b851c892d8c43329ca3beb5d2213ee87111af49884e3ce66cbd5ba5"); + } + + @Override + protected boolean isExpectedInstance(ProtectedStorageEntry data) { + return data.getProtectedStoragePayload() instanceof Arbitrator; + } + + @Override + protected void addAcceptedDisputeAgentToUser(Arbitrator disputeAgent) { + user.addAcceptedArbitrator(disputeAgent); + } + + @Override + protected void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data) { + user.removeAcceptedArbitrator((Arbitrator) data.getProtectedStoragePayload()); + } + + @Override + protected List getAcceptedDisputeAgentsFromUser() { + return user.getAcceptedArbitrators(); + } + + @Override + protected void clearAcceptedDisputeAgentsAtUser() { + user.clearAcceptedArbitrators(); + } + + @Override + protected Arbitrator getRegisteredDisputeAgentFromUser() { + return user.getRegisteredArbitrator(); + } + + @Override + protected void setRegisteredDisputeAgentAtUser(Arbitrator disputeAgent) { + user.setRegisteredArbitrator(disputeAgent); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorService.java b/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorService.java new file mode 100644 index 0000000000..34f19790ca --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/arbitrator/ArbitratorService.java @@ -0,0 +1,61 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.arbitration.arbitrator; + +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.agent.DisputeAgentService; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import com.google.inject.Singleton; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Singleton +public class ArbitratorService extends DisputeAgentService { + @Inject + public ArbitratorService(P2PService p2PService, FilterManager filterManager) { + super(p2PService, filterManager); + } + + @Override + protected Set getDisputeAgentSet(List bannedDisputeAgents) { + return p2PService.getDataMap().values().stream() + .filter(data -> data.getProtectedStoragePayload() instanceof Arbitrator) + .map(data -> (Arbitrator) data.getProtectedStoragePayload()) + .filter(a -> bannedDisputeAgents == null || + !bannedDisputeAgents.contains(a.getNodeAddress().getFullAddress())) + .collect(Collectors.toSet()); + } + + @Override + protected List getDisputeAgentsFromFilter() { + return filterManager.getFilter() != null ? filterManager.getFilter().getArbitrators() : new ArrayList<>(); + } + + public Map getArbitrators() { + return super.getDisputeAgents(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/messages/ArbitrationMessage.java b/core/src/main/java/bisq/core/support/dispute/arbitration/messages/ArbitrationMessage.java new file mode 100644 index 0000000000..b88ed9e601 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/messages/ArbitrationMessage.java @@ -0,0 +1,27 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.arbitration.messages; + +import bisq.core.support.SupportType; +import bisq.core.support.dispute.messages.DisputeMessage; + +abstract class ArbitrationMessage extends DisputeMessage { + ArbitrationMessage(int messageVersion, String uid, SupportType supportType) { + super(messageVersion, uid, supportType); + } +} diff --git a/core/src/main/java/bisq/core/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java b/core/src/main/java/bisq/core/support/dispute/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java similarity index 78% rename from core/src/main/java/bisq/core/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java rename to core/src/main/java/bisq/core/support/dispute/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java index 2d5d96f74e..17d9009044 100644 --- a/core/src/main/java/bisq/core/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/messages/PeerPublishedDisputePayoutTxMessage.java @@ -15,7 +15,9 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration.messages; +package bisq.core.support.dispute.arbitration.messages; + +import bisq.core.support.SupportType; import bisq.network.p2p.NodeAddress; @@ -29,7 +31,7 @@ import lombok.Value; @Value @EqualsAndHashCode(callSuper = true) -public final class PeerPublishedDisputePayoutTxMessage extends DisputeMessage { +public final class PeerPublishedDisputePayoutTxMessage extends ArbitrationMessage { private final byte[] transaction; private final String tradeId; private final NodeAddress senderNodeAddress; @@ -37,12 +39,14 @@ public final class PeerPublishedDisputePayoutTxMessage extends DisputeMessage { public PeerPublishedDisputePayoutTxMessage(byte[] transaction, String tradeId, NodeAddress senderNodeAddress, - String uid) { + String uid, + SupportType supportType) { this(transaction, tradeId, senderNodeAddress, uid, - Version.getP2PMessageVersion()); + Version.getP2PMessageVersion(), + supportType); } @@ -54,8 +58,9 @@ public final class PeerPublishedDisputePayoutTxMessage extends DisputeMessage { String tradeId, NodeAddress senderNodeAddress, String uid, - int messageVersion) { - super(messageVersion, uid); + int messageVersion, + SupportType supportType) { + super(messageVersion, uid, supportType); this.transaction = transaction; this.tradeId = tradeId; this.senderNodeAddress = senderNodeAddress; @@ -68,16 +73,19 @@ public final class PeerPublishedDisputePayoutTxMessage extends DisputeMessage { .setTransaction(ByteString.copyFrom(transaction)) .setTradeId(tradeId) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setUid(uid)) + .setUid(uid) + .setType(SupportType.toProtoMessage(supportType))) .build(); } - public static PeerPublishedDisputePayoutTxMessage fromProto(protobuf.PeerPublishedDisputePayoutTxMessage proto, int messageVersion) { + public static PeerPublishedDisputePayoutTxMessage fromProto(protobuf.PeerPublishedDisputePayoutTxMessage proto, + int messageVersion) { return new PeerPublishedDisputePayoutTxMessage(proto.getTransaction().toByteArray(), proto.getTradeId(), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getUid(), - messageVersion); + messageVersion, + SupportType.fromProto(proto.getType())); } @Override @@ -93,6 +101,7 @@ public final class PeerPublishedDisputePayoutTxMessage extends DisputeMessage { ",\n senderNodeAddress=" + senderNodeAddress + ",\n PeerPublishedDisputePayoutTxMessage.uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + + ",\n supportType=" + supportType + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeList.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeList.java new file mode 100644 index 0000000000..ce45e2fede --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeList.java @@ -0,0 +1,82 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.mediation; + +import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; + +import bisq.common.proto.ProtoUtil; +import bisq.common.storage.Storage; + +import com.google.protobuf.Message; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@ToString +/* + * Holds a List of mediation dispute objects. + * + * Calls to the List are delegated because this class intercepts the add/remove calls so changes + * can be saved to disc. + */ +public final class MediationDisputeList extends DisputeList { + + MediationDisputeList(Storage storage) { + super(storage); + } + + @Override + public void readPersisted() { + MediationDisputeList persisted = storage.initAndGetPersisted(this, "MediationDisputeList", 0); + if (persisted != null) { + list.addAll(persisted.getList()); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private MediationDisputeList(Storage storage, List list) { + super(storage, list); + } + + @Override + public Message toProtoMessage() { + return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder() + .addAllDispute(ProtoUtil.collectionToProto(new ArrayList<>(list)))).build(); + } + + public static MediationDisputeList fromProto(protobuf.MediationDisputeList proto, + CoreProtoResolver coreProtoResolver, + Storage storage) { + List list = proto.getDisputeList().stream() + .map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver)) + .collect(Collectors.toList()); + list.forEach(e -> e.setStorage(storage)); + return new MediationDisputeList(storage, list); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeListService.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeListService.java new file mode 100644 index 0000000000..cb4fbf4001 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationDisputeListService.java @@ -0,0 +1,48 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.mediation; + +import bisq.core.support.dispute.DisputeListService; + +import bisq.common.storage.Storage; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public final class MediationDisputeListService extends DisputeListService { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MediationDisputeListService(Storage storage) { + super(storage); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected MediationDisputeList getConcreteDisputeList() { + return new MediationDisputeList(storage); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java new file mode 100644 index 0000000000..592ff3fdd5 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationManager.java @@ -0,0 +1,251 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.mediation; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.messages.DisputeResultMessage; +import bisq.core.support.dispute.messages.OpenNewDisputeMessage; +import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage; +import bisq.core.support.messages.ChatMessage; +import bisq.core.support.messages.SupportMessage; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.protocol.ProcessModel; +import bisq.core.trade.protocol.TradeProtocol; + +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.crypto.PubKeyRing; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +@Singleton +public final class MediationManager extends DisputeManager { + + // The date when mediation is activated + private static final Date MEDIATION_ACTIVATED_DATE = Utilities.getUTCDate(2019, GregorianCalendar.SEPTEMBER, 1); + + public static boolean isMediationActivated() { + return new Date().after(MEDIATION_ACTIVATED_DATE); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MediationManager(P2PService p2PService, + TradeWalletService tradeWalletService, + BtcWalletService walletService, + WalletsSetup walletsSetup, + TradeManager tradeManager, + ClosedTradableManager closedTradableManager, + OpenOfferManager openOfferManager, + PubKeyRing pubKeyRing, + MediationDisputeListService mediationDisputeListService) { + super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, + openOfferManager, pubKeyRing, mediationDisputeListService); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public SupportType getSupportType() { + return SupportType.MEDIATION; + } + + @Override + public void dispatchMessage(SupportMessage message) { + if (canProcessMessage(message)) { + log.info("Received {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); + + if (message instanceof OpenNewDisputeMessage) { + onOpenNewDisputeMessage((OpenNewDisputeMessage) message); + } else if (message instanceof PeerOpenedDisputeMessage) { + onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message); + } else if (message instanceof ChatMessage) { + onChatMessage((ChatMessage) message); + } else if (message instanceof DisputeResultMessage) { + onDisputeResultMessage((DisputeResultMessage) message); + } else { + log.warn("Unsupported message at dispatchMessage. message={}", message); + } + } + } + + @Override + protected Trade.DisputeState getDisputeState_StartedByPeer() { + return Trade.DisputeState.MEDIATION_STARTED_BY_PEER; + } + + @Override + protected AckMessageSourceType getAckMessageSourceType() { + return AckMessageSourceType.MEDIATION_MESSAGE; + } + + @Override + public void cleanupDisputes() { + disputeListService.cleanupDisputes(tradeId -> { + tradeManager.getTradeById(tradeId).filter(trade -> trade.getPayoutTx() != null) + .ifPresent(trade -> { + tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED); + }); + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Message handler + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + // We get that message at both peers. The dispute object is in context of the trader + public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) { + DisputeResult disputeResult = disputeResultMessage.getDisputeResult(); + String tradeId = disputeResult.getTradeId(); + ChatMessage chatMessage = disputeResult.getChatMessage(); + checkNotNull(chatMessage, "chatMessage must not be null"); + Optional disputeOptional = findDispute(disputeResult); + String uid = disputeResultMessage.getUid(); + if (!disputeOptional.isPresent()) { + log.warn("We got a dispute result msg but we don't have a matching dispute. " + + "That might happen when we get the disputeResultMessage before the dispute was created. " + + "We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId); + if (!delayMsgMap.containsKey(uid)) { + // We delay 2 sec. to be sure the comm. msg gets added first + Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(disputeResultMessage), 2); + delayMsgMap.put(uid, timer); + } else { + log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " + + "That should never happen. TradeId = " + tradeId); + } + return; + } + + Dispute dispute = disputeOptional.get(); + cleanupRetryMap(uid); + if (!dispute.getChatMessages().contains(chatMessage)) { + dispute.addAndPersistChatMessage(chatMessage); + } else { + log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId()); + } + dispute.setIsClosed(true); + + if (dispute.disputeResultProperty().get() != null) { + log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " + + "again because the first close did not succeed. TradeId = " + tradeId); + } + + dispute.setDisputeResult(disputeResult); + + Optional tradeOptional = tradeManager.getTradeById(tradeId); + if (tradeOptional.isPresent()) { + Trade trade = tradeOptional.get(); + if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_REQUESTED || + trade.getDisputeState() == Trade.DisputeState.MEDIATION_STARTED_BY_PEER) { + trade.setDisputeState(Trade.DisputeState.MEDIATION_CLOSED); + } + } else { + Optional openOfferOptional = openOfferManager.getOpenOfferById(tradeId); + openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer())); + } + sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public NodeAddress getAgentNodeAddress(Dispute dispute) { + return dispute.getContract().getMediatorNodeAddress(); + } + + public void acceptMediationResult(Trade trade, + ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + String tradeId = trade.getId(); + Optional optionalDispute = findDispute(tradeId); + checkArgument(optionalDispute.isPresent(), "dispute must be present"); + DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get(); + Coin buyerPayoutAmount = disputeResult.getBuyerPayoutAmount(); + Coin sellerPayoutAmount = disputeResult.getSellerPayoutAmount(); + ProcessModel processModel = trade.getProcessModel(); + processModel.setBuyerPayoutAmountFromMediation(buyerPayoutAmount.value); + processModel.setSellerPayoutAmountFromMediation(sellerPayoutAmount.value); + TradeProtocol tradeProtocol = trade.getTradeProtocol(); + + trade.setMediationResultState(MediationResultState.MEDIATION_RESULT_ACCEPTED); + + // If we have not got yet the peers signature we sign and send to the peer our signature. + // Otherwise we sign and complete with the peers signature the payout tx. + if (processModel.getTradingPeer().getMediatedPayoutTxSignature() == null) { + tradeProtocol.onAcceptMediationResult(() -> { + if (trade.getPayoutTx() != null) { + tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED); + } + resultHandler.handleResult(); + }, errorMessageHandler); + } else { + tradeProtocol.onFinalizeMediationResultPayout(() -> { + if (trade.getPayoutTx() != null) { + tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED); + } + resultHandler.handleResult(); + }, errorMessageHandler); + } + } + + public void rejectMediationResult(Trade trade) { + trade.setMediationResultState(MediationResultState.MEDIATION_RESULT_REJECTED); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationResultState.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationResultState.java new file mode 100644 index 0000000000..0d9f0c93b2 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationResultState.java @@ -0,0 +1,46 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.mediation; + +import bisq.common.proto.ProtoUtil; + +public enum MediationResultState { + UNDEFINED_MEDIATION_RESULT, + MEDIATION_RESULT_ACCEPTED(), + MEDIATION_RESULT_REJECTED, + SIG_MSG_SENT, + SIG_MSG_ARRIVED, + SIG_MSG_IN_MAILBOX, + SIG_MSG_SEND_FAILED, + RECEIVED_SIG_MSG, + PAYOUT_TX_PUBLISHED, + PAYOUT_TX_PUBLISHED_MSG_SENT, + PAYOUT_TX_PUBLISHED_MSG_ARRIVED, + PAYOUT_TX_PUBLISHED_MSG_IN_MAILBOX, + PAYOUT_TX_PUBLISHED_MSG_SEND_FAILED, + RECEIVED_PAYOUT_TX_PUBLISHED_MSG, + PAYOUT_TX_SEEN_IN_NETWORK; + + public static MediationResultState fromProto(protobuf.MediationResultState mediationResultState) { + return ProtoUtil.enumFromProto(MediationResultState.class, mediationResultState.name()); + } + + public static protobuf.MediationResultState toProtoMessage(MediationResultState mediationResultState) { + return protobuf.MediationResultState.valueOf(mediationResultState.name()); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/MediationSession.java b/core/src/main/java/bisq/core/support/dispute/mediation/MediationSession.java new file mode 100644 index 0000000000..84fba15e6d --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/MediationSession.java @@ -0,0 +1,33 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.mediation; + +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeSession; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class MediationSession extends DisputeSession { + + public MediationSession(@Nullable Dispute dispute, boolean isTrader) { + super(dispute, isTrader); + } +} diff --git a/core/src/main/java/bisq/core/arbitration/Mediator.java b/core/src/main/java/bisq/core/support/dispute/mediation/mediator/Mediator.java similarity index 63% rename from core/src/main/java/bisq/core/arbitration/Mediator.java rename to core/src/main/java/bisq/core/support/dispute/mediation/mediator/Mediator.java index 30989cf80f..f02b4aff92 100644 --- a/core/src/main/java/bisq/core/arbitration/Mediator.java +++ b/core/src/main/java/bisq/core/support/dispute/mediation/mediator/Mediator.java @@ -15,57 +15,32 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration; +package bisq.core.support.dispute.mediation.mediator; + +import bisq.core.support.dispute.agent.DisputeAgent; import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.storage.payload.ExpirablePayload; -import bisq.network.p2p.storage.payload.ProtectedStoragePayload; import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; -import bisq.common.util.ExtraDataMapValidator; import com.google.protobuf.ByteString; import org.springframework.util.CollectionUtils; -import java.security.PublicKey; - +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.ToString; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; -@EqualsAndHashCode +@EqualsAndHashCode(callSuper = true) @Slf4j -@ToString -@Getter -public final class Mediator implements ProtectedStoragePayload, ExpirablePayload { - private final PubKeyRing pubKeyRing; - private final NodeAddress nodeAddress; - private final List languageCodes; - private final long registrationDate; - private final String registrationSignature; - private final byte[] registrationPubKey; - @Nullable - private final String emailAddress; - @Nullable - private final String info; - - // Should be only used in emergency case if we need to add data but do not want to break backward compatibility - // at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new - // field in a class would break that hash and therefore break the storage mechanism. - @Nullable - private Map extraDataMap; - +public final class Mediator extends DisputeAgent { public Mediator(NodeAddress nodeAddress, PubKeyRing pubKeyRing, List languageCodes, @@ -75,17 +50,17 @@ public final class Mediator implements ProtectedStoragePayload, ExpirablePayload @Nullable String emailAddress, @Nullable String info, @Nullable Map extraDataMap) { - this.nodeAddress = nodeAddress; - this.pubKeyRing = pubKeyRing; - this.languageCodes = languageCodes; - this.registrationDate = registrationDate; - this.registrationPubKey = registrationPubKey; - this.registrationSignature = registrationSignature; - this.emailAddress = emailAddress; - this.info = info; - this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap); - } + super(nodeAddress, + pubKeyRing, + languageCodes, + registrationDate, + registrationPubKey, + registrationSignature, + emailAddress, + info, + extraDataMap); + } /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER @@ -109,7 +84,7 @@ public final class Mediator implements ProtectedStoragePayload, ExpirablePayload public static Mediator fromProto(protobuf.Mediator proto) { return new Mediator(NodeAddress.fromProto(proto.getNodeAddress()), PubKeyRing.fromProto(proto.getPubKeyRing()), - proto.getLanguageCodesList().stream().collect(Collectors.toList()), + new ArrayList<>(proto.getLanguageCodesList()), proto.getRegistrationDate(), proto.getRegistrationPubKey().toByteArray(), proto.getRegistrationSignature(), @@ -118,17 +93,14 @@ public final class Mediator implements ProtectedStoragePayload, ExpirablePayload CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap()); } + /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// - @Override - public long getTTL() { - return TimeUnit.DAYS.toMillis(10); - } @Override - public PublicKey getOwnerPubKey() { - return pubKeyRing.getSignaturePubKey(); + public String toString() { + return "Mediator{} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorManager.java b/core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorManager.java new file mode 100644 index 0000000000..9a711ee24b --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorManager.java @@ -0,0 +1,101 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.mediation.mediator; + +import bisq.core.app.AppOptionKeys; +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.agent.DisputeAgentManager; +import bisq.core.user.User; + +import bisq.network.p2p.storage.payload.ProtectedStorageEntry; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.Singleton; +import com.google.inject.name.Named; + +import javax.inject.Inject; + +import java.util.List; + +@Singleton +public class MediatorManager extends DisputeAgentManager { + + @Inject + public MediatorManager(KeyRing keyRing, + MediatorService mediatorService, + User user, + FilterManager filterManager, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(keyRing, mediatorService, user, filterManager, useDevPrivilegeKeys); + } + + @Override + protected List getPubKeyList() { + return List.of("03be5471ff9090d322110d87912eefe89871784b1754d0707fdb917be5d88d3809", + "023736953a5a6638db71d7f78edc38cea0e42143c3b184ee67f331dafdc2c59efa", + "03d82260038253f7367012a4fc0c52dac74cfc67ac9cfbc3c3ad8fca746d8e5fc6", + "02dac85f726121ef333d425bc8e13173b5b365a6444176306e6a0a9e76ae1073bd", + "0342a5b37c8f843c3302e930d0197cdd8948a6f76747c05e138a6671a6a4caf739", + "027afa67c920867a70dfad77db6c6f74051f5af8bf56a1ad479f0bc4005df92325", + "03505f44f1893b64a457f8883afdd60774d7f4def6f82bb6f60be83a4b5b85cf82", + "0277d2d505d28ad67a03b001ef66f0eaaf1184fa87ebeaa937703cec7073cb2e8f", + "027cb3e9a56a438714e2144e2f75db7293ad967f12d5c29b17623efbd35ddbceb0", + "03be5471ff9090d322110d87912eefe89871784b1754d0707fdb917be5d88d3809", + "03756937d33d028eea274a3154775b2bffd076ffcc4a23fe0f9080f8b7fa0dab5b", + "03d8359823a91736cb7aecfaf756872daf258084133c9dd25b96ab3643707c38ca", + "03589ed6ded1a1aa92d6ad38bead13e4ad8ba24c60ca6ed8a8efc6e154e3f60add", + "0356965753f77a9c0e33ca7cc47fd43ce7f99b60334308ad3c11eed3665de79a78", + "031112eb033ebacb635754a2b7163c68270c9171c40f271e70e37b22a2590d3c18"); + } + + @Override + protected boolean isExpectedInstance(ProtectedStorageEntry data) { + return data.getProtectedStoragePayload() instanceof Mediator; + } + + @Override + protected void addAcceptedDisputeAgentToUser(Mediator disputeAgent) { + user.addAcceptedMediator(disputeAgent); + } + + @Override + protected void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data) { + user.removeAcceptedMediator((Mediator) data.getProtectedStoragePayload()); + } + + @Override + protected List getAcceptedDisputeAgentsFromUser() { + return user.getAcceptedMediators(); + } + + @Override + protected void clearAcceptedDisputeAgentsAtUser() { + user.clearAcceptedMediators(); + } + + @Override + protected Mediator getRegisteredDisputeAgentFromUser() { + return user.getRegisteredMediator(); + } + + @Override + protected void setRegisteredDisputeAgentAtUser(Mediator disputeAgent) { + user.setRegisteredMediator(disputeAgent); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorService.java b/core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorService.java new file mode 100644 index 0000000000..8ff78d2d7e --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/mediation/mediator/MediatorService.java @@ -0,0 +1,66 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.mediation.mediator; + +import bisq.core.filter.FilterManager; +import bisq.core.support.dispute.agent.DisputeAgentService; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import com.google.inject.Singleton; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + + +@Slf4j +@Singleton +public class MediatorService extends DisputeAgentService { + + @Inject + public MediatorService(P2PService p2PService, FilterManager filterManager) { + super(p2PService, filterManager); + } + + @Override + protected Set getDisputeAgentSet(List bannedDisputeAgents) { + return p2PService.getDataMap().values().stream() + .filter(data -> data.getProtectedStoragePayload() instanceof Mediator) + .map(data -> (Mediator) data.getProtectedStoragePayload()) + .filter(a -> bannedDisputeAgents == null || + !bannedDisputeAgents.contains(a.getNodeAddress().getFullAddress())) + .collect(Collectors.toSet()); + } + + @Override + protected List getDisputeAgentsFromFilter() { + return filterManager.getFilter() != null ? filterManager.getFilter().getMediators() : new ArrayList<>(); + } + + public Map getMediators() { + return super.getDisputeAgents(); + } +} diff --git a/core/src/main/java/bisq/core/support/dispute/messages/DisputeMessage.java b/core/src/main/java/bisq/core/support/dispute/messages/DisputeMessage.java new file mode 100644 index 0000000000..70167dea3f --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/messages/DisputeMessage.java @@ -0,0 +1,28 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.dispute.messages; + +import bisq.core.support.SupportType; +import bisq.core.support.messages.SupportMessage; + +public abstract class DisputeMessage extends SupportMessage { + + public DisputeMessage(int messageVersion, String uid, SupportType supportType) { + super(messageVersion, uid, supportType); + } +} diff --git a/core/src/main/java/bisq/core/arbitration/messages/DisputeResultMessage.java b/core/src/main/java/bisq/core/support/dispute/messages/DisputeResultMessage.java similarity index 80% rename from core/src/main/java/bisq/core/arbitration/messages/DisputeResultMessage.java rename to core/src/main/java/bisq/core/support/dispute/messages/DisputeResultMessage.java index 8af7cd0bb7..50f78529eb 100644 --- a/core/src/main/java/bisq/core/arbitration/messages/DisputeResultMessage.java +++ b/core/src/main/java/bisq/core/support/dispute/messages/DisputeResultMessage.java @@ -15,9 +15,10 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration.messages; +package bisq.core.support.dispute.messages; -import bisq.core.arbitration.DisputeResult; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.DisputeResult; import bisq.network.p2p.NodeAddress; @@ -36,11 +37,13 @@ public final class DisputeResultMessage extends DisputeMessage { public DisputeResultMessage(DisputeResult disputeResult, NodeAddress senderNodeAddress, - String uid) { + String uid, + SupportType supportType) { this(disputeResult, senderNodeAddress, uid, - Version.getP2PMessageVersion()); + Version.getP2PMessageVersion(), + supportType); } @@ -51,8 +54,9 @@ public final class DisputeResultMessage extends DisputeMessage { private DisputeResultMessage(DisputeResult disputeResult, NodeAddress senderNodeAddress, String uid, - int messageVersion) { - super(messageVersion, uid); + int messageVersion, + SupportType supportType) { + super(messageVersion, uid, supportType); this.disputeResult = disputeResult; this.senderNodeAddress = senderNodeAddress; } @@ -63,7 +67,8 @@ public final class DisputeResultMessage extends DisputeMessage { .setDisputeResultMessage(protobuf.DisputeResultMessage.newBuilder() .setDisputeResult(disputeResult.toProtoMessage()) .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) - .setUid(uid)) + .setUid(uid) + .setType(SupportType.toProtoMessage(supportType))) .build(); } @@ -72,7 +77,8 @@ public final class DisputeResultMessage extends DisputeMessage { return new DisputeResultMessage(DisputeResult.fromProto(proto.getDisputeResult()), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getUid(), - messageVersion); + messageVersion, + SupportType.fromProto(proto.getType())); } @Override @@ -87,6 +93,7 @@ public final class DisputeResultMessage extends DisputeMessage { ",\n senderNodeAddress=" + senderNodeAddress + ",\n DisputeResultMessage.uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + + ",\n supportType=" + supportType + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/arbitration/messages/OpenNewDisputeMessage.java b/core/src/main/java/bisq/core/support/dispute/messages/OpenNewDisputeMessage.java similarity index 81% rename from core/src/main/java/bisq/core/arbitration/messages/OpenNewDisputeMessage.java rename to core/src/main/java/bisq/core/support/dispute/messages/OpenNewDisputeMessage.java index ded2ff9453..f60cda8056 100644 --- a/core/src/main/java/bisq/core/arbitration/messages/OpenNewDisputeMessage.java +++ b/core/src/main/java/bisq/core/support/dispute/messages/OpenNewDisputeMessage.java @@ -15,10 +15,11 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration.messages; +package bisq.core.support.dispute.messages; -import bisq.core.arbitration.Dispute; import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; import bisq.network.p2p.NodeAddress; @@ -35,11 +36,13 @@ public final class OpenNewDisputeMessage extends DisputeMessage { public OpenNewDisputeMessage(Dispute dispute, NodeAddress senderNodeAddress, - String uid) { + String uid, + SupportType supportType) { this(dispute, senderNodeAddress, uid, - Version.getP2PMessageVersion()); + Version.getP2PMessageVersion(), + supportType); } @@ -50,8 +53,9 @@ public final class OpenNewDisputeMessage extends DisputeMessage { private OpenNewDisputeMessage(Dispute dispute, NodeAddress senderNodeAddress, String uid, - int messageVersion) { - super(messageVersion, uid); + int messageVersion, + SupportType supportType) { + super(messageVersion, uid, supportType); this.dispute = dispute; this.senderNodeAddress = senderNodeAddress; } @@ -62,7 +66,8 @@ public final class OpenNewDisputeMessage extends DisputeMessage { .setOpenNewDisputeMessage(protobuf.OpenNewDisputeMessage.newBuilder() .setUid(uid) .setDispute(dispute.toProtoMessage()) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage())) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setType(SupportType.toProtoMessage(supportType))) .build(); } @@ -72,7 +77,8 @@ public final class OpenNewDisputeMessage extends DisputeMessage { return new OpenNewDisputeMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getUid(), - messageVersion); + messageVersion, + SupportType.fromProto(proto.getType())); } @Override @@ -87,6 +93,7 @@ public final class OpenNewDisputeMessage extends DisputeMessage { ",\n senderNodeAddress=" + senderNodeAddress + ",\n OpenNewDisputeMessage.uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + + ",\n supportType=" + supportType + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/arbitration/messages/PeerOpenedDisputeMessage.java b/core/src/main/java/bisq/core/support/dispute/messages/PeerOpenedDisputeMessage.java similarity index 80% rename from core/src/main/java/bisq/core/arbitration/messages/PeerOpenedDisputeMessage.java rename to core/src/main/java/bisq/core/support/dispute/messages/PeerOpenedDisputeMessage.java index fd741e9509..f2dc136acc 100644 --- a/core/src/main/java/bisq/core/arbitration/messages/PeerOpenedDisputeMessage.java +++ b/core/src/main/java/bisq/core/support/dispute/messages/PeerOpenedDisputeMessage.java @@ -15,10 +15,11 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration.messages; +package bisq.core.support.dispute.messages; -import bisq.core.arbitration.Dispute; import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; import bisq.network.p2p.NodeAddress; @@ -35,11 +36,13 @@ public final class PeerOpenedDisputeMessage extends DisputeMessage { public PeerOpenedDisputeMessage(Dispute dispute, NodeAddress senderNodeAddress, - String uid) { + String uid, + SupportType supportType) { this(dispute, senderNodeAddress, uid, - Version.getP2PMessageVersion()); + Version.getP2PMessageVersion(), + supportType); } @@ -50,8 +53,9 @@ public final class PeerOpenedDisputeMessage extends DisputeMessage { private PeerOpenedDisputeMessage(Dispute dispute, NodeAddress senderNodeAddress, String uid, - int messageVersion) { - super(messageVersion, uid); + int messageVersion, + SupportType supportType) { + super(messageVersion, uid, supportType); this.dispute = dispute; this.senderNodeAddress = senderNodeAddress; } @@ -62,7 +66,8 @@ public final class PeerOpenedDisputeMessage extends DisputeMessage { .setPeerOpenedDisputeMessage(protobuf.PeerOpenedDisputeMessage.newBuilder() .setUid(uid) .setDispute(dispute.toProtoMessage()) - .setSenderNodeAddress(senderNodeAddress.toProtoMessage())) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setType(SupportType.toProtoMessage(supportType))) .build(); } @@ -70,7 +75,8 @@ public final class PeerOpenedDisputeMessage extends DisputeMessage { return new PeerOpenedDisputeMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver), NodeAddress.fromProto(proto.getSenderNodeAddress()), proto.getUid(), - messageVersion); + messageVersion, + SupportType.fromProto(proto.getType())); } @Override @@ -85,6 +91,7 @@ public final class PeerOpenedDisputeMessage extends DisputeMessage { ",\n senderNodeAddress=" + senderNodeAddress + ",\n PeerOpenedDisputeMessage.uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + + ",\n supportType=" + supportType + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java b/core/src/main/java/bisq/core/support/messages/ChatMessage.java similarity index 72% rename from core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java rename to core/src/main/java/bisq/core/support/messages/ChatMessage.java index c198c64f8e..51c4f2a06d 100644 --- a/core/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java +++ b/core/src/main/java/bisq/core/support/messages/ChatMessage.java @@ -15,14 +15,14 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration.messages; +package bisq.core.support.messages; -import bisq.core.arbitration.Attachment; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Attachment; import bisq.network.p2p.NodeAddress; import bisq.common.app.Version; -import bisq.common.proto.ProtoUtil; import bisq.common.util.Utilities; import javafx.beans.property.BooleanProperty; @@ -58,29 +58,12 @@ import javax.annotation.Nullable; @EqualsAndHashCode(callSuper = true) // listener is transient and therefore excluded anyway @Getter @Slf4j -public final class DisputeCommunicationMessage extends DisputeMessage { - - public enum Type { - MEDIATION, - ARBITRATION, - TRADE; - - public static DisputeCommunicationMessage.Type fromProto( - protobuf.DisputeCommunicationMessage.Type type) { - return ProtoUtil.enumFromProto(DisputeCommunicationMessage.Type.class, type.name()); - } - - public static protobuf.DisputeCommunicationMessage.Type toProtoMessage(Type type) { - return protobuf.DisputeCommunicationMessage.Type.valueOf(type.name()); - } - } +public final class ChatMessage extends SupportMessage { public interface Listener { void onMessageStateChanged(); } - // Added with v1.1.6. Old clients will not have set that field and we fall back to entry 0 which is DISPUTE. - private final DisputeCommunicationMessage.Type type; private final String tradeId; private final int traderId; // This is only used for the server client relationship @@ -93,10 +76,11 @@ public final class DisputeCommunicationMessage extends DisputeMessage { @Setter private boolean isSystemMessage; - // Added in v1.1.6. + // Added in v1.1.6. for trader chat to store if message was shown in popup @Setter private boolean wasDisplayed; + //todo move to base class private final BooleanProperty arrivedProperty; private final BooleanProperty storedInMailboxProperty; private final BooleanProperty acknowledgedProperty; @@ -105,13 +89,13 @@ public final class DisputeCommunicationMessage extends DisputeMessage { transient private WeakReference listener; - public DisputeCommunicationMessage(DisputeCommunicationMessage.Type type, - String tradeId, - int traderId, - boolean senderIsTrader, - String message, - NodeAddress senderNodeAddress) { - this(type, + public ChatMessage(SupportType supportType, + String tradeId, + int traderId, + boolean senderIsTrader, + String message, + NodeAddress senderNodeAddress) { + this(supportType, tradeId, traderId, senderIsTrader, @@ -129,14 +113,39 @@ public final class DisputeCommunicationMessage extends DisputeMessage { false); } - public DisputeCommunicationMessage(DisputeCommunicationMessage.Type type, - String tradeId, - int traderId, - boolean senderIsTrader, - String message, - NodeAddress senderNodeAddress, - long date) { - this(type, + public ChatMessage(SupportType supportType, + String tradeId, + int traderId, + boolean senderIsTrader, + String message, + NodeAddress senderNodeAddress, + ArrayList attachments) { + this(supportType, + tradeId, + traderId, + senderIsTrader, + message, + attachments, + senderNodeAddress, + new Date().getTime(), + false, + false, + UUID.randomUUID().toString(), + Version.getP2PMessageVersion(), + false, + null, + null, + false); + } + + public ChatMessage(SupportType supportType, + String tradeId, + int traderId, + boolean senderIsTrader, + String message, + NodeAddress senderNodeAddress, + long date) { + this(supportType, tradeId, traderId, senderIsTrader, @@ -159,24 +168,23 @@ public final class DisputeCommunicationMessage extends DisputeMessage { // PROTO BUFFER /////////////////////////////////////////////////////////////////////////////////////////// - private DisputeCommunicationMessage(Type type, - String tradeId, - int traderId, - boolean senderIsTrader, - String message, - @Nullable List attachments, - NodeAddress senderNodeAddress, - long date, - boolean arrived, - boolean storedInMailbox, - String uid, - int messageVersion, - boolean acknowledged, - @Nullable String sendMessageError, - @Nullable String ackError, - boolean wasDisplayed) { - super(messageVersion, uid); - this.type = type; + private ChatMessage(SupportType supportType, + String tradeId, + int traderId, + boolean senderIsTrader, + String message, + @Nullable List attachments, + NodeAddress senderNodeAddress, + long date, + boolean arrived, + boolean storedInMailbox, + String uid, + int messageVersion, + boolean acknowledged, + @Nullable String sendMessageError, + @Nullable String ackError, + boolean wasDisplayed) { + super(messageVersion, uid, supportType); this.tradeId = tradeId; this.traderId = traderId; this.senderIsTrader = senderIsTrader; @@ -193,10 +201,11 @@ public final class DisputeCommunicationMessage extends DisputeMessage { notifyChangeListener(); } + // We cannot rename protobuf definition because it would break backward compatibility @Override public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { - protobuf.DisputeCommunicationMessage.Builder builder = protobuf.DisputeCommunicationMessage.newBuilder() - .setType(DisputeCommunicationMessage.Type.toProtoMessage(type)) + protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder() + .setType(SupportType.toProtoMessage(supportType)) .setTradeId(tradeId) .setTraderId(traderId) .setSenderIsTrader(senderIsTrader) @@ -213,17 +222,17 @@ public final class DisputeCommunicationMessage extends DisputeMessage { Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError); Optional.ofNullable(ackErrorProperty.get()).ifPresent(builder::setAckError); return getNetworkEnvelopeBuilder() - .setDisputeCommunicationMessage(builder) + .setChatMessage(builder) .build(); } - public static DisputeCommunicationMessage fromProto(protobuf.DisputeCommunicationMessage proto, - int messageVersion) { + // The protobuf definition ChatMessage cannot be changed as it would break backward compatibility. + public static ChatMessage fromProto(protobuf.ChatMessage proto, + int messageVersion) { // If we get a msg from an old client type will be ordinal 0 which is the dispute entry and as we only added // the trade case it is the desired behaviour. - DisputeCommunicationMessage.Type type = DisputeCommunicationMessage.Type.fromProto(proto.getType()); - final DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( - type, + final ChatMessage chatMessage = new ChatMessage( + SupportType.fromProto(proto.getType()), proto.getTradeId(), proto.getTraderId(), proto.getSenderIsTrader(), @@ -239,11 +248,11 @@ public final class DisputeCommunicationMessage extends DisputeMessage { proto.getSendMessageError().isEmpty() ? null : proto.getSendMessageError(), proto.getAckError().isEmpty() ? null : proto.getAckError(), proto.getWasDisplayed()); - disputeCommunicationMessage.setSystemMessage(proto.getIsSystemMessage()); - return disputeCommunicationMessage; + chatMessage.setSystemMessage(proto.getIsSystemMessage()); + return chatMessage; } - public static DisputeCommunicationMessage fromPayloadProto(protobuf.DisputeCommunicationMessage proto) { + public static ChatMessage fromPayloadProto(protobuf.ChatMessage proto) { // We have the case that an envelope got wrapped into a payload. // We don't check the message version here as it was checked in the carrier envelope already (in connection class) // Payloads don't have a message version and are also used for persistence @@ -256,7 +265,7 @@ public final class DisputeCommunicationMessage extends DisputeMessage { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void addAllAttachments(List attachments) { + private void addAllAttachments(List attachments) { this.attachments.addAll(attachments); } @@ -330,8 +339,8 @@ public final class DisputeCommunicationMessage extends DisputeMessage { @Override public String toString() { - return "DisputeCommunicationMessage{" + - "\n type='" + type + '\'' + + return "ChatMessage{" + + "\n type='" + supportType + '\'' + ",\n tradeId='" + tradeId + '\'' + ",\n traderId=" + traderId + ",\n senderIsTrader=" + senderIsTrader + @@ -342,7 +351,7 @@ public final class DisputeCommunicationMessage extends DisputeMessage { ",\n isSystemMessage=" + isSystemMessage + ",\n arrivedProperty=" + arrivedProperty + ",\n storedInMailboxProperty=" + storedInMailboxProperty + - ",\n DisputeCommunicationMessage.uid='" + uid + '\'' + + ",\n ChatMessage.uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + ",\n acknowledgedProperty=" + acknowledgedProperty + ",\n sendMessageErrorProperty=" + sendMessageErrorProperty + diff --git a/core/src/main/java/bisq/core/arbitration/messages/DisputeMessage.java b/core/src/main/java/bisq/core/support/messages/SupportMessage.java similarity index 73% rename from core/src/main/java/bisq/core/arbitration/messages/DisputeMessage.java rename to core/src/main/java/bisq/core/support/messages/SupportMessage.java index 1eb3428392..fae761210f 100644 --- a/core/src/main/java/bisq/core/arbitration/messages/DisputeMessage.java +++ b/core/src/main/java/bisq/core/support/messages/SupportMessage.java @@ -15,7 +15,9 @@ * along with Bisq. If not, see . */ -package bisq.core.arbitration.messages; +package bisq.core.support.messages; + +import bisq.core.support.SupportType; import bisq.network.p2p.MailboxMessage; import bisq.network.p2p.UidMessage; @@ -27,12 +29,16 @@ import lombok.Getter; @EqualsAndHashCode(callSuper = true) @Getter -public abstract class DisputeMessage extends NetworkEnvelope implements MailboxMessage, UidMessage { +public abstract class SupportMessage extends NetworkEnvelope implements MailboxMessage, UidMessage { protected final String uid; - public DisputeMessage(int messageVersion, String uid) { + // Added with v1.1.6. Old clients will not have set that field and we fall back to entry 0 which is ARBITRATION. + protected final SupportType supportType; + + public SupportMessage(int messageVersion, String uid, SupportType supportType) { super(messageVersion); this.uid = uid; + this.supportType = supportType; } public abstract String getTradeId(); @@ -42,6 +48,7 @@ public abstract class DisputeMessage extends NetworkEnvelope implements MailboxM return "DisputeMessage{" + "\n uid='" + uid + '\'' + ",\n messageVersion=" + messageVersion + + ",\n supportType=" + supportType + "\n} " + super.toString(); } } diff --git a/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java b/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java new file mode 100644 index 0000000000..c465d5ff72 --- /dev/null +++ b/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java @@ -0,0 +1,73 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.traderchat; + +import bisq.core.support.SupportSession; +import bisq.core.support.messages.ChatMessage; +import bisq.core.trade.Trade; + +import bisq.common.crypto.PubKeyRing; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class TradeChatSession extends SupportSession { + + @Nullable + private Trade trade; + + public TradeChatSession(@Nullable Trade trade, + boolean isClient) { + super(isClient); + this.trade = trade; + } + + @Override + public String getTradeId() { + return trade != null ? trade.getId() : ""; + } + + @Override + public PubKeyRing getClientPubKeyRing() { + // TODO remove that client-server concept for trade chat + // Get pubKeyRing of taker. Maker is considered server for chat sessions + if (trade != null && trade.getContract() != null) + return trade.getContract().getTakerPubKeyRing(); + return null; + } + + @Override + public ObservableList getObservableChatMessageList() { + return trade != null ? trade.getChatMessages() : FXCollections.observableArrayList(); + } + + @Override + public boolean chatIsOpen() { + return trade != null && trade.getState() != Trade.State.WITHDRAW_COMPLETED; + } + + @Override + public boolean isDisputeAgent() { + return false; + } +} diff --git a/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java b/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java new file mode 100644 index 0000000000..de5659e5bf --- /dev/null +++ b/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java @@ -0,0 +1,187 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.support.traderchat; + +import bisq.core.btc.setup.WalletsSetup; +import bisq.core.locale.Res; +import bisq.core.support.SupportManager; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.messages.DisputeResultMessage; +import bisq.core.support.messages.ChatMessage; +import bisq.core.support.messages.SupportMessage; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; + +import bisq.network.p2p.AckMessageSourceType; +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.PubKeyRing; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import javafx.collections.ObservableList; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Singleton +public class TraderChatManager extends SupportManager { + private final TradeManager tradeManager; + private final PubKeyRing pubKeyRing; + + public interface DisputeStateListener { + void onDisputeClosed(String tradeId); + } + + // Needed to avoid ConcurrentModificationException as we remove a listener at the handler call + private List disputeStateListeners = new CopyOnWriteArrayList<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public TraderChatManager(P2PService p2PService, + WalletsSetup walletsSetup, + TradeManager tradeManager, + PubKeyRing pubKeyRing) { + super(p2PService, walletsSetup); + this.tradeManager = tradeManager; + this.pubKeyRing = pubKeyRing; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Implement template methods + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public SupportType getSupportType() { + return SupportType.TRADE; + } + + @Override + public void persist() { + tradeManager.persistTrades(); + } + + @Override + public NodeAddress getPeerNodeAddress(ChatMessage message) { + return tradeManager.getTradeById(message.getTradeId()).map(trade -> { + if (trade.getContract() != null) { + return trade.getContract().getPeersNodeAddress(pubKeyRing); + } else { + return null; + } + }).orElse(null); + } + + @Override + public PubKeyRing getPeerPubKeyRing(ChatMessage message) { + return tradeManager.getTradeById(message.getTradeId()).map(trade -> { + if (trade.getContract() != null) { + return trade.getContract().getPeersPubKeyRing(pubKeyRing); + } else { + return null; + } + }).orElse(null); + } + + @Override + public List getAllChatMessages() { + return tradeManager.getTradableList().stream() + .flatMap(trade -> trade.getChatMessages().stream()) + .collect(Collectors.toList()); + } + + @Override + public boolean channelOpen(ChatMessage message) { + return tradeManager.getTradeById(message.getTradeId()).isPresent(); + } + + @Override + public void addAndPersistChatMessage(ChatMessage message) { + tradeManager.getTradeById(message.getTradeId()).ifPresent(trade -> { + ObservableList chatMessages = trade.getChatMessages(); + if (chatMessages.stream().noneMatch(m -> m.getUid().equals(message.getUid()))) { + if (chatMessages.isEmpty()) { + addSystemMsg(trade); + } + trade.addAndPersistChatMessage(message); + } else { + log.warn("Trade got a chatMessage what we have already stored. UId = {} TradeId = {}", + message.getUid(), message.getTradeId()); + } + }); + } + + @Override + protected AckMessageSourceType getAckMessageSourceType() { + return AckMessageSourceType.TRADE_CHAT_MESSAGE; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addDisputeStateListener(TraderChatManager.DisputeStateListener disputeStateListener) { + disputeStateListeners.add(disputeStateListener); + } + + public void removeDisputeStateListener(TraderChatManager.DisputeStateListener disputeStateListener) { + disputeStateListeners.remove(disputeStateListener); + } + + public void dispatchMessage(SupportMessage message) { + if (canProcessMessage(message)) { + log.info("Received {} with tradeId {} and uid {}", + message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); + if (message instanceof ChatMessage) { + onChatMessage((ChatMessage) message); + } else if (message instanceof DisputeResultMessage) { + // We notify about dispute closed state + disputeStateListeners.forEach(e -> e.onDisputeClosed(message.getTradeId())); + } else { + log.warn("Unsupported message at dispatchMessage. message={}", message); + } + } + } + + public void addSystemMsg(Trade trade) { + // We need to use the trade date as otherwise our system msg would not be displayed first as the list is sorted + // by date. + ChatMessage chatMessage = new ChatMessage( + getSupportType(), + trade.getId(), + 0, + false, + Res.get("tradeChat.rules"), + new NodeAddress("null:0000"), + trade.getDate().getTime()); + chatMessage.setSystemMessage(true); + trade.getChatMessages().add(chatMessage); + } +} diff --git a/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java b/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java index b4de75c558..17eeca7a7a 100644 --- a/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java +++ b/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java @@ -47,9 +47,17 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { Coin takeOfferFee, boolean isCurrencyForTakerFeeBtc, @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, Storage storage, BtcWalletService btcWalletService) { - super(offer, txFee, takeOfferFee, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, storage, btcWalletService); + super(offer, + txFee, + takeOfferFee, + isCurrencyForTakerFeeBtc, + arbitratorNodeAddress, + mediatorNodeAddress, + storage, + btcWalletService); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -75,6 +83,7 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { Coin.valueOf(proto.getTakerFeeAsLong()), proto.getIsCurrencyForTakerFeeBtc(), proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, + proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, storage, btcWalletService); @@ -98,7 +107,9 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade { } @Override - public void handleTakeOfferRequest(TradeMessage message, NodeAddress taker, ErrorMessageHandler errorMessageHandler) { + public void handleTakeOfferRequest(TradeMessage message, + NodeAddress taker, + ErrorMessageHandler errorMessageHandler) { ((MakerProtocol) tradeProtocol).handleTakeOfferRequest(message, taker, errorMessageHandler); } } diff --git a/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java b/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java index e1d4d52b85..c4c2c1e776 100644 --- a/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java +++ b/core/src/main/java/bisq/core/trade/BuyerAsTakerTrade.java @@ -50,10 +50,20 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade { long tradePrice, NodeAddress tradingPeerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, Storage storage, BtcWalletService btcWalletService) { - super(offer, tradeAmount, txFee, takerFee, isCurrencyForTakerFeeBtc, tradePrice, tradingPeerNodeAddress, - arbitratorNodeAddress, storage, btcWalletService); + super(offer, + tradeAmount, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + tradePrice, + tradingPeerNodeAddress, + arbitratorNodeAddress, + mediatorNodeAddress, + storage, + btcWalletService); } @@ -83,6 +93,7 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade { proto.getTradePrice(), proto.hasTradingPeerNodeAddress() ? NodeAddress.fromProto(proto.getTradingPeerNodeAddress()) : null, proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, + proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, storage, btcWalletService), proto, diff --git a/core/src/main/java/bisq/core/trade/BuyerTrade.java b/core/src/main/java/bisq/core/trade/BuyerTrade.java index 4d4c297135..4c534b30f0 100644 --- a/core/src/main/java/bisq/core/trade/BuyerTrade.java +++ b/core/src/main/java/bisq/core/trade/BuyerTrade.java @@ -46,10 +46,20 @@ public abstract class BuyerTrade extends Trade { long tradePrice, NodeAddress tradingPeerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, Storage storage, BtcWalletService btcWalletService) { - super(offer, tradeAmount, txFee, takerFee, isCurrencyForTakerFeeBtc, tradePrice, - tradingPeerNodeAddress, arbitratorNodeAddress, storage, btcWalletService); + super(offer, + tradeAmount, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + tradePrice, + tradingPeerNodeAddress, + arbitratorNodeAddress, + mediatorNodeAddress, + storage, + btcWalletService); } BuyerTrade(Offer offer, @@ -57,9 +67,17 @@ public abstract class BuyerTrade extends Trade { Coin takerFee, boolean isCurrencyForTakerFeeBtc, @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, Storage storage, BtcWalletService btcWalletService) { - super(offer, txFee, takerFee, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, storage, btcWalletService); + super(offer, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + arbitratorNodeAddress, + mediatorNodeAddress, + storage, + btcWalletService); } public void onFiatPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { diff --git a/core/src/main/java/bisq/core/trade/Contract.java b/core/src/main/java/bisq/core/trade/Contract.java index b78219497f..6ec2e57394 100644 --- a/core/src/main/java/bisq/core/trade/Contract.java +++ b/core/src/main/java/bisq/core/trade/Contract.java @@ -236,6 +236,31 @@ public final class Contract implements NetworkPayload { return Price.valueOf(offerPayload.getCurrencyCode(), tradePrice); } + public NodeAddress getMyNodeAddress(PubKeyRing myPubKeyRing) { + if (myPubKeyRing.equals(getBuyerPubKeyRing())) + return buyerNodeAddress; + else + return sellerNodeAddress; + } + + public NodeAddress getPeersNodeAddress(PubKeyRing myPubKeyRing) { + if (myPubKeyRing.equals(getSellerPubKeyRing())) + return buyerNodeAddress; + else + return sellerNodeAddress; + } + + public PubKeyRing getPeersPubKeyRing(PubKeyRing myPubKeyRing) { + if (myPubKeyRing.equals(getSellerPubKeyRing())) + return getBuyerPubKeyRing(); + else + return getSellerPubKeyRing(); + } + + public boolean isMyRoleBuyer(PubKeyRing myPubKeyRing) { + return getBuyerPubKeyRing().equals(myPubKeyRing); + } + public void printDiff(@Nullable String peersContractAsJson) { final String json = Utilities.objectToJson(this); String diff = StringUtils.difference(json, peersContractAsJson); diff --git a/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java b/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java index 072f54b602..08fb2f46ca 100644 --- a/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java +++ b/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java @@ -47,9 +47,10 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade Coin takerFee, boolean isCurrencyForTakerFeeBtc, @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, Storage storage, BtcWalletService btcWalletService) { - super(offer, txFee, takerFee, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, storage, btcWalletService); + super(offer, txFee, takerFee, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, mediatorNodeAddress, storage, btcWalletService); } @@ -76,6 +77,7 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade Coin.valueOf(proto.getTakerFeeAsLong()), proto.getIsCurrencyForTakerFeeBtc(), proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, + proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, storage, btcWalletService); diff --git a/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java b/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java index 22881b56b2..0d4aeb6b84 100644 --- a/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java +++ b/core/src/main/java/bisq/core/trade/SellerAsTakerTrade.java @@ -50,10 +50,20 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade long tradePrice, NodeAddress tradingPeerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, Storage storage, BtcWalletService btcWalletService) { - super(offer, tradeAmount, txFee, takerFee, isCurrencyForTakerFeeBtc, tradePrice, - tradingPeerNodeAddress, arbitratorNodeAddress, storage, btcWalletService); + super(offer, + tradeAmount, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + tradePrice, + tradingPeerNodeAddress, + arbitratorNodeAddress, + mediatorNodeAddress, + storage, + btcWalletService); } @@ -83,6 +93,7 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade proto.getTradePrice(), proto.hasTradingPeerNodeAddress() ? NodeAddress.fromProto(proto.getTradingPeerNodeAddress()) : null, proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null, + proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null, storage, btcWalletService), proto, diff --git a/core/src/main/java/bisq/core/trade/SellerTrade.java b/core/src/main/java/bisq/core/trade/SellerTrade.java index 0d56eadd85..629dc5adb5 100644 --- a/core/src/main/java/bisq/core/trade/SellerTrade.java +++ b/core/src/main/java/bisq/core/trade/SellerTrade.java @@ -45,10 +45,20 @@ public abstract class SellerTrade extends Trade { long tradePrice, NodeAddress tradingPeerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, Storage storage, BtcWalletService btcWalletService) { - super(offer, tradeAmount, txFee, takerFee, isCurrencyForTakerFeeBtc, tradePrice, - tradingPeerNodeAddress, arbitratorNodeAddress, storage, btcWalletService); + super(offer, + tradeAmount, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + tradePrice, + tradingPeerNodeAddress, + arbitratorNodeAddress, + mediatorNodeAddress, + storage, + btcWalletService); } SellerTrade(Offer offer, @@ -56,9 +66,17 @@ public abstract class SellerTrade extends Trade { Coin takeOfferFee, boolean isCurrencyForTakerFeeBtc, @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, Storage storage, BtcWalletService btcWalletService) { - super(offer, txFee, takeOfferFee, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, storage, btcWalletService); + super(offer, + txFee, + takeOfferFee, + isCurrencyForTakerFeeBtc, + arbitratorNodeAddress, + mediatorNodeAddress, + storage, + btcWalletService); } public void onFiatPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index 2a4ae411d5..f88b50ca04 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -18,10 +18,6 @@ package bisq.core.trade; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.arbitration.Arbitrator; -import bisq.core.arbitration.ArbitratorManager; -import bisq.core.arbitration.Mediator; -import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; @@ -34,6 +30,11 @@ import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.payload.PaymentMethod; import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.messages.ChatMessage; import bisq.core.trade.protocol.ProcessModel; import bisq.core.trade.protocol.TradeProtocol; import bisq.core.trade.statistics.ReferralIdService; @@ -44,7 +45,6 @@ import bisq.network.p2p.DecryptedMessageWithPubKey; import bisq.network.p2p.NodeAddress; import bisq.network.p2p.P2PService; -import bisq.common.UserThread; import bisq.common.crypto.KeyRing; import bisq.common.crypto.PubKeyRing; import bisq.common.proto.ProtoUtil; @@ -213,9 +213,15 @@ public abstract class Trade implements Tradable, Model { public enum DisputeState { NO_DISPUTE, + // arbitration DISPUTE_REQUESTED, DISPUTE_STARTED_BY_PEER, - DISPUTE_CLOSED; + DISPUTE_CLOSED, + + // mediation + MEDIATION_REQUESTED, + MEDIATION_STARTED_BY_PEER, + MEDIATION_CLOSED; public static Trade.DisputeState fromProto(protobuf.Trade.DisputeState disputeState) { return ProtoUtil.enumFromProto(Trade.DisputeState.class, disputeState.name()); @@ -311,6 +317,7 @@ public abstract class Trade implements Tradable, Model { private String makerContractSignature; @Nullable @Getter + @Setter private NodeAddress arbitratorNodeAddress; @Nullable @Setter @@ -321,6 +328,7 @@ public abstract class Trade implements Tradable, Model { private PubKeyRing arbitratorPubKeyRing; @Nullable @Getter + @Setter private NodeAddress mediatorNodeAddress; @Nullable @Getter @@ -337,7 +345,7 @@ public abstract class Trade implements Tradable, Model { @Nullable private String counterCurrencyTxId; @Getter - private final ObservableList communicationMessages = FXCollections.observableArrayList(); + private final ObservableList chatMessages = FXCollections.observableArrayList(); // Transient // Immutable @@ -370,6 +378,12 @@ public abstract class Trade implements Tradable, Model { transient private ObjectProperty tradeVolumeProperty; final transient private Set decryptedMessageWithPubKeySet = new HashSet<>(); + //Added in v1.1.6 + @Getter + @Nullable + private MediationResultState mediationResultState = MediationResultState.UNDEFINED_MEDIATION_RESULT; + transient final private ObjectProperty mediationResultStateProperty = new SimpleObjectProperty<>(mediationResultState); + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization @@ -381,6 +395,7 @@ public abstract class Trade implements Tradable, Model { Coin takerFee, boolean isCurrencyForTakerFeeBtc, @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, Storage storage, BtcWalletService btcWalletService) { this.offer = offer; @@ -390,6 +405,7 @@ public abstract class Trade implements Tradable, Model { this.storage = storage; this.btcWalletService = btcWalletService; this.arbitratorNodeAddress = arbitratorNodeAddress; + this.mediatorNodeAddress = mediatorNodeAddress; txFeeAsLong = txFee.value; takerFeeAsLong = takerFee.value; @@ -408,10 +424,18 @@ public abstract class Trade implements Tradable, Model { long tradePrice, NodeAddress tradingPeerNodeAddress, @Nullable NodeAddress arbitratorNodeAddress, + @Nullable NodeAddress mediatorNodeAddress, Storage storage, BtcWalletService btcWalletService) { - this(offer, txFee, takerFee, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, storage, btcWalletService); + this(offer, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + arbitratorNodeAddress, + mediatorNodeAddress, + storage, + btcWalletService); this.tradePrice = tradePrice; this.tradingPeerNodeAddress = tradingPeerNodeAddress; @@ -426,7 +450,7 @@ public abstract class Trade implements Tradable, Model { @Override public Message toProtoMessage() { final protobuf.Trade.Builder builder = protobuf.Trade.newBuilder() - .setOffer(offer.toProtoMessage()) + .setOffer(checkNotNull(offer).toProtoMessage()) .setIsCurrencyForTakerFeeBtc(isCurrencyForTakerFeeBtc) .setTxFeeAsLong(txFeeAsLong) .setTakerFeeAsLong(takerFeeAsLong) @@ -434,11 +458,11 @@ public abstract class Trade implements Tradable, Model { .setProcessModel(processModel.toProtoMessage()) .setTradeAmountAsLong(tradeAmountAsLong) .setTradePrice(tradePrice) - .setState(protobuf.Trade.State.valueOf(state.name())) - .setDisputeState(protobuf.Trade.DisputeState.valueOf(disputeState.name())) - .setTradePeriodState(protobuf.Trade.TradePeriodState.valueOf(tradePeriodState.name())) - .addAllCommunicationMessages(communicationMessages.stream() - .map(msg -> msg.toProtoNetworkEnvelope().getDisputeCommunicationMessage()) + .setState(Trade.State.toProtoMessage(state)) + .setDisputeState(Trade.DisputeState.toProtoMessage(disputeState)) + .setTradePeriodState(Trade.TradePeriodState.toProtoMessage(tradePeriodState)) + .addAllChatMessage(chatMessages.stream() + .map(msg -> msg.toProtoNetworkEnvelope().getChatMessage()) .collect(Collectors.toList())); Optional.ofNullable(takerFeeTxId).ifPresent(builder::setTakerFeeTxId); @@ -458,6 +482,7 @@ public abstract class Trade implements Tradable, Model { Optional.ofNullable(arbitratorPubKeyRing).ifPresent(e -> builder.setArbitratorPubKeyRing(arbitratorPubKeyRing.toProtoMessage())); Optional.ofNullable(mediatorPubKeyRing).ifPresent(e -> builder.setMediatorPubKeyRing(mediatorPubKeyRing.toProtoMessage())); Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId)); + Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState))); return builder.build(); } @@ -483,9 +508,10 @@ public abstract class Trade implements Tradable, Model { trade.setArbitratorPubKeyRing(proto.hasArbitratorPubKeyRing() ? PubKeyRing.fromProto(proto.getArbitratorPubKeyRing()) : null); trade.setMediatorPubKeyRing(proto.hasMediatorPubKeyRing() ? PubKeyRing.fromProto(proto.getMediatorPubKeyRing()) : null); trade.setCounterCurrencyTxId(proto.getCounterCurrencyTxId().isEmpty() ? null : proto.getCounterCurrencyTxId()); + trade.setMediationResultState(MediationResultState.fromProto(proto.getMediationResultState())); - trade.communicationMessages.addAll(proto.getCommunicationMessagesList().stream() - .map(DisputeCommunicationMessage::fromPayloadProto) + trade.chatMessages.addAll(proto.getChatMessageList().stream() + .map(ChatMessage::fromPayloadProto) .collect(Collectors.toList())); return trade; @@ -513,10 +539,11 @@ public abstract class Trade implements Tradable, Model { AccountAgeWitnessService accountAgeWitnessService, TradeStatisticsManager tradeStatisticsManager, ArbitratorManager arbitratorManager, + MediatorManager mediatorManager, KeyRing keyRing, boolean useSavingsWallet, Coin fundsNeededForTrade) { - processModel.onAllServicesInitialized(offer, + processModel.onAllServicesInitialized(checkNotNull(offer, "offer must not be null"), tradeManager, openOfferManager, p2PService, @@ -529,17 +556,21 @@ public abstract class Trade implements Tradable, Model { accountAgeWitnessService, tradeStatisticsManager, arbitratorManager, + mediatorManager, keyRing, useSavingsWallet, fundsNeededForTrade); - Optional optionalArbitrator = processModel.getArbitratorManager().getArbitratorByNodeAddress(arbitratorNodeAddress); - if (optionalArbitrator.isPresent()) { - Arbitrator arbitrator = optionalArbitrator.get(); + arbitratorManager.getDisputeAgentByNodeAddress(arbitratorNodeAddress).ifPresent(arbitrator -> { arbitratorBtcPubKey = arbitrator.getBtcPubKey(); arbitratorPubKeyRing = arbitrator.getPubKeyRing(); - UserThread.runAfter(this::persist, 1); - } + persist(); + }); + + mediatorManager.getDisputeAgentByNodeAddress(mediatorNodeAddress).ifPresent(mediator -> { + mediatorPubKeyRing = mediator.getPubKeyRing(); + persist(); + }); createTradeProtocol(); @@ -557,12 +588,11 @@ public abstract class Trade implements Tradable, Model { /////////////////////////////////////////////////////////////////////////////////////////// // The deserialized tx has not actual confidence data, so we need to get the fresh one from the wallet. - public void updateDepositTxFromWallet() { + void updateDepositTxFromWallet() { if (getDepositTx() != null) setDepositTx(processModel.getTradeWalletService().getWalletTx(getDepositTx().getHash())); } - @SuppressWarnings("NullableProblems") public void setDepositTx(Transaction tx) { log.debug("setDepositTx " + tx); this.depositTx = tx; @@ -581,7 +611,7 @@ public abstract class Trade implements Tradable, Model { // We don't need to persist the msg as if we dont apply it it will not be removed from the P2P network and we // will received it again at next startup. Such might happen in edge cases when the user shuts down after we // received the msb but before the init is called. - public void addDecryptedMessageWithPubKey(DecryptedMessageWithPubKey decryptedMessageWithPubKey) { + void addDecryptedMessageWithPubKey(DecryptedMessageWithPubKey decryptedMessageWithPubKey) { if (!decryptedMessageWithPubKeySet.contains(decryptedMessageWithPubKey)) { decryptedMessageWithPubKeySet.add(decryptedMessageWithPubKey); @@ -598,12 +628,12 @@ public abstract class Trade implements Tradable, Model { decryptedMessageWithPubKeySet.remove(decryptedMessageWithPubKey); } - public void addCommunicationMessage(DisputeCommunicationMessage disputeCommunicationMessage) { - if (!communicationMessages.contains(disputeCommunicationMessage)) { - communicationMessages.add(disputeCommunicationMessage); + public void addAndPersistChatMessage(ChatMessage chatMessage) { + if (!chatMessages.contains(chatMessage)) { + chatMessages.add(chatMessage); storage.queueUpForSave(); } else { - log.error("Trade DisputeCommunicationMessage already exists"); + log.error("Trade ChatMessage already exists"); } } @@ -628,9 +658,9 @@ public abstract class Trade implements Tradable, Model { // Abstract /////////////////////////////////////////////////////////////////////////////////////////// - abstract protected void createTradeProtocol(); + protected abstract void createTradeProtocol(); - abstract public Coin getPayoutAmount(); + public abstract Coin getPayoutAmount(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -665,6 +695,14 @@ public abstract class Trade implements Tradable, Model { persist(); } + public void setMediationResultState(MediationResultState mediationResultState) { + boolean changed = this.mediationResultState != mediationResultState; + this.mediationResultState = mediationResultState; + mediationResultStateProperty.set(mediationResultState); + if (changed) + persist(); + } + public void setTradePeriodState(TradePeriodState tradePeriodState) { boolean changed = this.tradePeriodState != tradePeriodState; @@ -674,7 +712,6 @@ public abstract class Trade implements Tradable, Model { persist(); } - @SuppressWarnings("NullableProblems") public void setTradingPeerNodeAddress(NodeAddress tradingPeerNodeAddress) { if (tradingPeerNodeAddress == null) log.error("tradingPeerAddress=null"); @@ -682,7 +719,6 @@ public abstract class Trade implements Tradable, Model { this.tradingPeerNodeAddress = tradingPeerNodeAddress; } - @SuppressWarnings("NullableProblems") public void setTradeAmount(Coin tradeAmount) { this.tradeAmount = tradeAmount; tradeAmountAsLong = tradeAmount.value; @@ -690,42 +726,16 @@ public abstract class Trade implements Tradable, Model { getTradeVolumeProperty().set(getTradeVolume()); } - @SuppressWarnings("NullableProblems") public void setPayoutTx(Transaction payoutTx) { this.payoutTx = payoutTx; payoutTxId = payoutTx.getHashAsString(); } - @SuppressWarnings("NullableProblems") public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; errorMessageProperty.set(errorMessage); } - //TODO can be removed after new rule is actiavted - @SuppressWarnings("NullableProblems") - public void setArbitratorNodeAddress(NodeAddress arbitratorNodeAddress) { - this.arbitratorNodeAddress = arbitratorNodeAddress; - if (processModel.getUser() != null) { - Arbitrator arbitrator = processModel.getUser().getAcceptedArbitratorByAddress(arbitratorNodeAddress); - checkNotNull(arbitrator, "arbitrator must not be null"); - arbitratorBtcPubKey = arbitrator.getBtcPubKey(); - arbitratorPubKeyRing = arbitrator.getPubKeyRing(); - persist(); - } - } - - @SuppressWarnings("NullableProblems") - public void setMediatorNodeAddress(NodeAddress mediatorNodeAddress) { - this.mediatorNodeAddress = mediatorNodeAddress; - if (processModel.getUser() != null) { - Mediator mediator = processModel.getUser().getAcceptedMediatorByAddress(mediatorNodeAddress); - checkNotNull(mediator, "mediator must not be null"); - mediatorPubKeyRing = mediator.getPubKeyRing(); - persist(); - } - } - /////////////////////////////////////////////////////////////////////////////////////////// // Getter @@ -847,6 +857,10 @@ public abstract class Trade implements Tradable, Model { return disputeStateProperty; } + public ReadOnlyObjectProperty mediationResultStateProperty() { + return mediationResultStateProperty; + } + public ReadOnlyObjectProperty tradePeriodStateProperty() { return tradePeriodStateProperty; } @@ -1012,7 +1026,7 @@ public abstract class Trade implements Tradable, Model { ",\n decryptedMessageWithPubKeySet=" + decryptedMessageWithPubKeySet + ",\n arbitratorPubKeyRing=" + arbitratorPubKeyRing + ",\n mediatorPubKeyRing=" + mediatorPubKeyRing + - ",\n communicationMessages=" + communicationMessages + + ",\n chatMessages=" + chatMessages + "\n}"; } } diff --git a/core/src/main/java/bisq/core/trade/TradeChatSession.java b/core/src/main/java/bisq/core/trade/TradeChatSession.java deleted file mode 100644 index c26f9bc73b..0000000000 --- a/core/src/main/java/bisq/core/trade/TradeChatSession.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - -package bisq.core.trade; - -import bisq.core.arbitration.messages.DisputeCommunicationMessage; -import bisq.core.arbitration.messages.DisputeMessage; -import bisq.core.arbitration.messages.DisputeResultMessage; -import bisq.core.chat.ChatManager; -import bisq.core.chat.ChatSession; -import bisq.core.locale.Res; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.crypto.PubKeyRing; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; - -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.stream.Collectors; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.Nullable; - -/* Makers are considered as servers and takers as clients for trader to trader chat - * sessions. This is only to make it easier to understand who's who, there is no real - * server/client relationship */ -public class TradeChatSession extends ChatSession { - private static final Logger log = LoggerFactory.getLogger(TradeChatSession.class); - - public interface DisputeStateListener { - void onDisputeClosed(String tradeId); - } - - @Nullable - private Trade trade; - private boolean isClient; - private boolean isBuyer; - private TradeManager tradeManager; - private ChatManager chatManager; - // Needed to avoid ConcurrentModificationException as we remove a listener at the handler call - private List disputeStateListeners = new CopyOnWriteArrayList<>(); - - public TradeChatSession(@Nullable Trade trade, - boolean isClient, - boolean isBuyer, - TradeManager tradeManager, - ChatManager chatManager) { - super(DisputeCommunicationMessage.Type.TRADE); - this.trade = trade; - this.isClient = isClient; - this.isBuyer = isBuyer; - this.tradeManager = tradeManager; - this.chatManager = chatManager; - } - - public void addDisputeStateListener(DisputeStateListener disputeStateListener) { - disputeStateListeners.add(disputeStateListener); - } - - public void removeDisputeStateListener(DisputeStateListener disputeStateListener) { - disputeStateListeners.remove(disputeStateListener); - } - - @Override - public boolean isClient() { - return isClient; - } - - @Override - public String getTradeId() { - return trade != null ? trade.getId() : ""; - } - - @Override - public PubKeyRing getClientPubKeyRing() { - // Get pubkeyring of taker. Maker is considered server for chat sessions - if (trade != null && trade.getContract() != null) - return trade.getContract().getTakerPubKeyRing(); - return null; - } - - @Override - public void addDisputeCommunicationMessage(DisputeCommunicationMessage message) { - if (trade != null) - trade.addCommunicationMessage(message); - } - - @Override - public void persist() { - tradeManager.persistTrades(); - } - - @Override - public ObservableList getDisputeCommunicationMessages() { - return trade != null ? trade.getCommunicationMessages() : FXCollections.observableArrayList(); - } - - @Override - public boolean chatIsOpen() { - return trade != null && trade.getState() != Trade.State.WITHDRAW_COMPLETED; - } - - @Override - public NodeAddress getPeerNodeAddress(DisputeCommunicationMessage message) { - Optional tradeOptional = tradeManager.getTradeById(message.getTradeId()); - if (tradeOptional.isPresent()) { - Trade t = tradeOptional.get(); - if (t.getContract() != null) - return isBuyer ? - t.getContract().getSellerNodeAddress() : - t.getContract().getBuyerNodeAddress(); - } - return null; - } - - @Override - public PubKeyRing getPeerPubKeyRing(DisputeCommunicationMessage message) { - Optional tradeOptional = tradeManager.getTradeById(message.getTradeId()); - if (tradeOptional.isPresent()) { - Trade t = tradeOptional.get(); - if (t.getContract() != null && t.getOffer() != null) { - if (t.getOffer().getOwnerPubKey().equals(tradeManager.getKeyRing().getPubKeyRing().getSignaturePubKey())) { - // I am maker - return t.getContract().getTakerPubKeyRing(); - } else { - return t.getContract().getMakerPubKeyRing(); - } - } - } - return null; - } - - @Override - public void dispatchMessage(DisputeMessage message) { - log.info("Received {} with tradeId {} and uid {}", - message.getClass().getSimpleName(), message.getTradeId(), message.getUid()); - if (message instanceof DisputeCommunicationMessage) { - if (((DisputeCommunicationMessage) message).getType() == DisputeCommunicationMessage.Type.TRADE) { - chatManager.onDisputeDirectMessage((DisputeCommunicationMessage) message); - } - // We ignore dispute messages - } else if (message instanceof DisputeResultMessage) { - // We notify about dispute closed state - disputeStateListeners.forEach(e -> e.onDisputeClosed(message.getTradeId())); - } - // We ignore all other non DisputeCommunicationMessages - } - - @Override - public List getChatMessages() { - return tradeManager.getTradableList().stream() - .flatMap(trade -> trade.getCommunicationMessages().stream()) - .collect(Collectors.toList()); - } - - @Override - public boolean channelOpen(DisputeCommunicationMessage message) { - return tradeManager.getTradeById(message.getTradeId()).isPresent(); - } - - @Override - public void storeDisputeCommunicationMessage(DisputeCommunicationMessage message) { - Optional tradeOptional = tradeManager.getTradeById(message.getTradeId()); - if (tradeOptional.isPresent()) { - Trade trade = tradeOptional.get(); - ObservableList communicationMessages = trade.getCommunicationMessages(); - if (communicationMessages.stream().noneMatch(m -> m.getUid().equals(message.getUid()))) { - if (communicationMessages.isEmpty()) { - addSystemMsg(trade); - } - trade.addCommunicationMessage(message); - } else { - log.warn("Trade got a disputeCommunicationMessage what we have already stored. UId = {} TradeId = {}", - message.getUid(), message.getTradeId()); - } - } - } - - public void addSystemMsg(Trade trade) { - // We need to use the trade date as otherwise our system msg would not be displayed first as the list is sorted - // by date. - DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage( - DisputeCommunicationMessage.Type.TRADE, - trade.getId(), - 0, - false, - Res.get("tradeChat.rules"), - new NodeAddress("null:0000"), - trade.getDate().getTime() - ); - disputeCommunicationMessage.setSystemMessage(true); - trade.getCommunicationMessages().add(disputeCommunicationMessage); - } -} diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 2d7a8f5968..d3e5739888 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -18,14 +18,11 @@ package bisq.core.trade; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.arbitration.ArbitratorManager; import bisq.core.btc.exceptions.AddressEntryException; import bisq.core.btc.model.AddressEntry; -import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; -import bisq.core.chat.ChatManager; import bisq.core.filter.FilterManager; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; @@ -33,6 +30,8 @@ import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; import bisq.core.offer.availability.OfferAvailabilityModel; import bisq.core.provider.price.PriceFeedService; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.failed.FailedTradesManager; import bisq.core.trade.handlers.TradeResultHandler; @@ -116,6 +115,7 @@ public class TradeManager implements PersistedDataHost { private final ReferralIdService referralIdService; private final AccountAgeWitnessService accountAgeWitnessService; private final ArbitratorManager arbitratorManager; + private final MediatorManager mediatorManager; private final ClockWatcher clockWatcher; private final Storage> tradableListStorage; @@ -128,9 +128,6 @@ public class TradeManager implements PersistedDataHost { @Getter private final LongProperty numPendingTrades = new SimpleLongProperty(); - @Getter - private final ChatManager chatManager; - /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -146,13 +143,13 @@ public class TradeManager implements PersistedDataHost { ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager, P2PService p2PService, - WalletsSetup walletsSetup, PriceFeedService priceFeedService, FilterManager filterManager, TradeStatisticsManager tradeStatisticsManager, ReferralIdService referralIdService, AccountAgeWitnessService accountAgeWitnessService, ArbitratorManager arbitratorManager, + MediatorManager mediatorManager, ClockWatcher clockWatcher, Storage> storage) { this.user = user; @@ -170,13 +167,11 @@ public class TradeManager implements PersistedDataHost { this.referralIdService = referralIdService; this.accountAgeWitnessService = accountAgeWitnessService; this.arbitratorManager = arbitratorManager; + this.mediatorManager = mediatorManager; this.clockWatcher = clockWatcher; tradableListStorage = storage; - chatManager = new ChatManager(p2PService, walletsSetup); - chatManager.setChatSession(new TradeChatSession(null, true, true, this, chatManager)); - p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, peerNodeAddress) -> { NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope(); @@ -249,8 +244,6 @@ public class TradeManager implements PersistedDataHost { log.warn("Swapping pending OFFER_FUNDING entries at startup. offerId={}", addressEntry.getOfferId()); btcWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), AddressEntry.Context.OFFER_FUNDING); }); - - chatManager.onAllServicesInitialized(); } public void shutDown() { @@ -337,6 +330,7 @@ public class TradeManager implements PersistedDataHost { Coin.valueOf(payDepositRequest.getTakerFee()), payDepositRequest.isCurrencyForTakerFeeBtc(), openOffer.getArbitratorNodeAddress(), + openOffer.getMediatorNodeAddress(), tradableListStorage, btcWalletService); else @@ -345,6 +339,7 @@ public class TradeManager implements PersistedDataHost { Coin.valueOf(payDepositRequest.getTakerFee()), payDepositRequest.isCurrencyForTakerFeeBtc(), openOffer.getArbitratorNodeAddress(), + openOffer.getMediatorNodeAddress(), tradableListStorage, btcWalletService); @@ -375,6 +370,7 @@ public class TradeManager implements PersistedDataHost { accountAgeWitnessService, tradeStatisticsManager, arbitratorManager, + mediatorManager, keyRing, useSavingsWallet, fundsNeededForTrade); @@ -456,6 +452,7 @@ public class TradeManager implements PersistedDataHost { tradePrice, model.getPeerNodeAddress(), model.getSelectedArbitrator(), + model.getSelectedMediator(), tradableListStorage, btcWalletService); else @@ -467,6 +464,7 @@ public class TradeManager implements PersistedDataHost { tradePrice, model.getPeerNodeAddress(), model.getSelectedArbitrator(), + model.getSelectedMediator(), tradableListStorage, btcWalletService); @@ -484,7 +482,9 @@ public class TradeManager implements PersistedDataHost { offer, keyRing.getPubKeyRing(), p2PService, - user); + user, + mediatorManager, + tradeStatisticsManager); } @@ -557,11 +557,11 @@ public class TradeManager implements PersistedDataHost { // Dispute /////////////////////////////////////////////////////////////////////////////////////////// - public void closeDisputedTrade(String tradeId) { + public void closeDisputedTrade(String tradeId, Trade.DisputeState disputeState) { Optional tradeOptional = getTradeById(tradeId); if (tradeOptional.isPresent()) { Trade trade = tradeOptional.get(); - trade.setDisputeState(Trade.DisputeState.DISPUTE_CLOSED); + trade.setDisputeState(disputeState); addTradeToClosedTrades(trade); btcWalletService.swapTradeEntryToAvailableEntry(trade.getId(), AddressEntry.Context.TRADE_PAYOUT); } diff --git a/core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxPublishedMessage.java new file mode 100644 index 0000000000..e20daba473 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxPublishedMessage.java @@ -0,0 +1,91 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.trade.messages; + +import bisq.network.p2p.MailboxMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.proto.network.NetworkEnvelope; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class MediatedPayoutTxPublishedMessage extends TradeMessage implements MailboxMessage { + private final byte[] payoutTx; + private final NodeAddress senderNodeAddress; + + public MediatedPayoutTxPublishedMessage(String tradeId, + byte[] payoutTx, + NodeAddress senderNodeAddress, + String uid) { + this(tradeId, + payoutTx, + senderNodeAddress, + uid, + Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private MediatedPayoutTxPublishedMessage(String tradeId, + byte[] payoutTx, + NodeAddress senderNodeAddress, + String uid, + int messageVersion) { + super(messageVersion, tradeId, uid); + this.payoutTx = payoutTx; + this.senderNodeAddress = senderNodeAddress; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setMediatedPayoutTxPublishedMessage(protobuf.MediatedPayoutTxPublishedMessage.newBuilder() + .setTradeId(tradeId) + .setPayoutTx(ByteString.copyFrom(payoutTx)) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setUid(uid)) + .build(); + } + + public static NetworkEnvelope fromProto(protobuf.MediatedPayoutTxPublishedMessage proto, int messageVersion) { + return new MediatedPayoutTxPublishedMessage(proto.getTradeId(), + proto.getPayoutTx().toByteArray(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getUid(), + messageVersion); + } + + @Override + public String toString() { + return "MediatedPayoutTxPublishedMessage{" + + "\n payoutTx=" + Utilities.bytesAsHexString(payoutTx) + + ",\n senderNodeAddress=" + senderNodeAddress + + ",\n uid='" + uid + '\'' + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxSignatureMessage.java b/core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxSignatureMessage.java new file mode 100644 index 0000000000..b476576b65 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/MediatedPayoutTxSignatureMessage.java @@ -0,0 +1,99 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.trade.messages; + +import bisq.network.p2p.MailboxMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Value +@EqualsAndHashCode(callSuper = true) +public class MediatedPayoutTxSignatureMessage extends TradeMessage implements MailboxMessage { + private final byte[] txSignature; + private final NodeAddress senderNodeAddress; + + public MediatedPayoutTxSignatureMessage(byte[] txSignature, + String tradeId, + NodeAddress senderNodeAddress, + String uid) { + this(txSignature, + tradeId, + senderNodeAddress, + uid, + Version.getP2PMessageVersion()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private MediatedPayoutTxSignatureMessage(byte[] txSignature, + String tradeId, + NodeAddress senderNodeAddress, + String uid, + int messageVersion) { + super(messageVersion, tradeId, uid); + this.txSignature = txSignature; + this.senderNodeAddress = senderNodeAddress; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setMediatedPayoutTxSignatureMessage(protobuf.MediatedPayoutTxSignatureMessage.newBuilder() + .setTxSignature(ByteString.copyFrom(txSignature)) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setUid(uid)) + .build(); + } + + public static MediatedPayoutTxSignatureMessage fromProto(protobuf.MediatedPayoutTxSignatureMessage proto, + int messageVersion) { + return new MediatedPayoutTxSignatureMessage(proto.getTxSignature().toByteArray(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getUid(), + messageVersion); + } + + @Override + public String getTradeId() { + return tradeId; + } + + + @Override + public String toString() { + return "MediatedPayoutSignatureMessage{" + + "\n txSignature=" + Utilities.bytesAsHexString(txSignature) + + ",\n tradeId='" + tradeId + '\'' + + ",\n senderNodeAddress=" + senderNodeAddress + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/PayDepositRequest.java b/core/src/main/java/bisq/core/trade/messages/PayDepositRequest.java index 95909a9a07..05447f26a6 100644 --- a/core/src/main/java/bisq/core/trade/messages/PayDepositRequest.java +++ b/core/src/main/java/bisq/core/trade/messages/PayDepositRequest.java @@ -155,7 +155,9 @@ public final class PayDepositRequest extends TradeMessage { return getNetworkEnvelopeBuilder().setPayDepositRequest(builder).build(); } - public static PayDepositRequest fromProto(protobuf.PayDepositRequest proto, CoreProtoResolver coreProtoResolver, int messageVersion) { + public static PayDepositRequest fromProto(protobuf.PayDepositRequest proto, + CoreProtoResolver coreProtoResolver, + int messageVersion) { List rawTransactionInputs = proto.getRawTransactionInputsList().stream() .map(rawTransactionInput -> new RawTransactionInput(rawTransactionInput.getIndex(), rawTransactionInput.getParentTransaction().toByteArray(), rawTransactionInput.getValue())) diff --git a/core/src/main/java/bisq/core/trade/protocol/ArbitratorSelectionRule.java b/core/src/main/java/bisq/core/trade/protocol/ArbitratorSelectionRule.java deleted file mode 100644 index d598e5f6c2..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/ArbitratorSelectionRule.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - -package bisq.core.trade.protocol; - -import bisq.core.offer.Offer; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.crypto.Hash; - -import com.google.common.base.Charsets; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import lombok.extern.slf4j.Slf4j; - -import static com.google.common.base.Preconditions.checkArgument; - -@Slf4j -public class ArbitratorSelectionRule { - public static NodeAddress select(List acceptedArbitratorNodeAddresses, Offer offer) { - List candidates = new ArrayList<>(); - for (NodeAddress offerArbitratorNodeAddress : offer.getArbitratorNodeAddresses()) { - candidates.addAll(acceptedArbitratorNodeAddresses.stream().filter(offerArbitratorNodeAddress::equals).collect(Collectors.toList())); - } - checkArgument(candidates.size() > 0, "candidates.size() <= 0"); - - int index = Math.abs(Arrays.hashCode(Hash.getSha256Hash(offer.getId().getBytes(Charsets.UTF_8)))) % candidates.size(); - NodeAddress selectedArbitrator = candidates.get(index); - log.debug("selectedArbitrator " + selectedArbitrator); - return selectedArbitrator; - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java index 54da527916..dd68fd16f0 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java @@ -115,7 +115,9 @@ public class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void handleTakeOfferRequest(TradeMessage tradeMessage, NodeAddress peerNodeAddress, ErrorMessageHandler errorMessageHandler) { + public void handleTakeOfferRequest(TradeMessage tradeMessage, + NodeAddress peerNodeAddress, + ErrorMessageHandler errorMessageHandler) { Validator.checkTradeId(processModel.getOfferId(), tradeMessage); checkArgument(tradeMessage instanceof PayDepositRequest); processModel.setTradeMessage(tradeMessage); @@ -225,6 +227,8 @@ public class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol @Override protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress sender) { + super.doHandleDecryptedMessage(tradeMessage, sender); + log.info("Received {} from {} with tradeId {} and uid {}", tradeMessage.getClass().getSimpleName(), sender, tradeMessage.getTradeId(), tradeMessage.getUid()); @@ -232,11 +236,6 @@ public class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol handle((DepositTxPublishedMessage) tradeMessage, sender); } else if (tradeMessage instanceof PayoutTxPublishedMessage) { handle((PayoutTxPublishedMessage) tradeMessage, sender); - } else //noinspection StatementWithEmptyBody - if (tradeMessage instanceof PayDepositRequest) { - // do nothing as we get called the handleTakeOfferRequest method from outside - } else { - log.error("Incoming decrypted tradeMessage not supported. " + tradeMessage); - } + } } } diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java index ae7d9b09ba..36b868cd90 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java @@ -35,7 +35,6 @@ import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerSignAndPublishD import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx; import bisq.core.trade.protocol.tasks.taker.TakerProcessPublishDepositTxRequest; import bisq.core.trade.protocol.tasks.taker.TakerPublishFeeTx; -import bisq.core.trade.protocol.tasks.taker.TakerSelectMediator; import bisq.core.trade.protocol.tasks.taker.TakerSendDepositTxPublishedMessage; import bisq.core.trade.protocol.tasks.taker.TakerSendPayDepositRequest; import bisq.core.trade.protocol.tasks.taker.TakerVerifyAndSignContract; @@ -114,7 +113,6 @@ public class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol this::handleTaskRunnerFault); taskRunner.addTasks( - TakerSelectMediator.class, TakerVerifyMakerAccount.class, TakerVerifyMakerFeePayment.class, CreateTakerFeeTx.class, @@ -192,7 +190,6 @@ public class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol } } - /////////////////////////////////////////////////////////////////////////////////////////// // Incoming message handling /////////////////////////////////////////////////////////////////////////////////////////// @@ -212,12 +209,15 @@ public class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol taskRunner.run(); } + /////////////////////////////////////////////////////////////////////////////////////////// // Massage dispatcher /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress sender) { + super.doHandleDecryptedMessage(tradeMessage, sender); + log.info("Received {} from {} with tradeId {} and uid {}", tradeMessage.getClass().getSimpleName(), sender, tradeMessage.getTradeId(), tradeMessage.getUid()); @@ -225,8 +225,6 @@ public class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol handle((PublishDepositTxRequest) tradeMessage, sender); } else if (tradeMessage instanceof PayoutTxPublishedMessage) { handle((PayoutTxPublishedMessage) tradeMessage, sender); - } else { - log.error("Incoming message not supported. " + tradeMessage); } } } diff --git a/core/src/main/java/bisq/core/trade/protocol/MediatorSelectionRule.java b/core/src/main/java/bisq/core/trade/protocol/MediatorSelectionRule.java deleted file mode 100644 index 2aefb627d8..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/MediatorSelectionRule.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - -package bisq.core.trade.protocol; - -import bisq.core.offer.Offer; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.crypto.Hash; - -import com.google.common.base.Charsets; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import lombok.extern.slf4j.Slf4j; - -import static com.google.common.base.Preconditions.checkArgument; - -@Slf4j -public class MediatorSelectionRule { - public static NodeAddress select(List acceptedMediatorNodeAddresses, Offer offer) { - List candidates = new ArrayList<>(); - for (NodeAddress offerMediatorNodeAddress : offer.getMediatorNodeAddresses()) { - candidates.addAll(acceptedMediatorNodeAddresses.stream() - .filter(offerMediatorNodeAddress::equals) - .collect(Collectors.toList())); - } - checkArgument(candidates.size() > 0, "candidates.size() <= 0"); - - int index = Math.abs(Arrays.hashCode(Hash.getSha256Hash(offer.getId().getBytes(Charsets.UTF_8)))) % candidates.size(); - NodeAddress selectedMediator = candidates.get(index); - log.debug("selectedMediator " + selectedMediator); - return selectedMediator; - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java index 185dcee0cc..f2b180926a 100644 --- a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java @@ -18,7 +18,6 @@ package bisq.core.trade.protocol; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.arbitration.ArbitratorManager; import bisq.core.btc.model.RawTransactionInput; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; @@ -30,6 +29,8 @@ import bisq.core.offer.OpenOfferManager; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.trade.MakerTrade; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; @@ -83,6 +84,7 @@ public class ProcessModel implements Model, PersistablePayload { transient private AccountAgeWitnessService accountAgeWitnessService; transient private TradeStatisticsManager tradeStatisticsManager; transient private ArbitratorManager arbitratorManager; + transient private MediatorManager mediatorManager; transient private KeyRing keyRing; transient private P2PService p2PService; transient private ReferralIdService referralIdService; @@ -142,6 +144,15 @@ public class ProcessModel implements Model, PersistablePayload { @Setter private NodeAddress tempTradingPeerNodeAddress; + // Added in v.1.1.6 + @Nullable + @Setter + private byte[] mediatedPayoutTxSignature; + @Setter + private long buyerPayoutAmountFromMediation; + @Setter + private long sellerPayoutAmountFromMediation; + // The only trade message where we want to indicate the user the state of the message delivery is the // CounterCurrencyTransferStartedMessage. We persist the state with the processModel. @Setter @@ -165,7 +176,9 @@ public class ProcessModel implements Model, PersistablePayload { .setChangeOutputValue(changeOutputValue) .setUseSavingsWallet(useSavingsWallet) .setFundsNeededForTradeAsLong(fundsNeededForTradeAsLong) - .setPaymentStartedMessageState(paymentStartedMessageStateProperty.get().name()); + .setPaymentStartedMessageState(paymentStartedMessageStateProperty.get().name()) + .setBuyerPayoutAmountFromMediation(buyerPayoutAmountFromMediation) + .setSellerPayoutAmountFromMediation(sellerPayoutAmountFromMediation); Optional.ofNullable(takeOfferFeeTxId).ifPresent(builder::setTakeOfferFeeTxId); Optional.ofNullable(payoutTxSignature).ifPresent(e -> builder.setPayoutTxSignature(ByteString.copyFrom(payoutTxSignature))); @@ -176,6 +189,7 @@ public class ProcessModel implements Model, PersistablePayload { Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress); Optional.ofNullable(myMultiSigPubKey).ifPresent(e -> builder.setMyMultiSigPubKey(ByteString.copyFrom(myMultiSigPubKey))); Optional.ofNullable(tempTradingPeerNodeAddress).ifPresent(e -> builder.setTempTradingPeerNodeAddress(tempTradingPeerNodeAddress.toProtoMessage())); + Optional.ofNullable(mediatedPayoutTxSignature).ifPresent(e -> builder.setMediatedPayoutTxSignature(ByteString.copyFrom(e))); return builder.build(); } @@ -188,6 +202,8 @@ public class ProcessModel implements Model, PersistablePayload { processModel.setChangeOutputValue(proto.getChangeOutputValue()); processModel.setUseSavingsWallet(proto.getUseSavingsWallet()); processModel.setFundsNeededForTradeAsLong(proto.getFundsNeededForTradeAsLong()); + processModel.setBuyerPayoutAmountFromMediation(proto.getBuyerPayoutAmountFromMediation()); + processModel.setSellerPayoutAmountFromMediation(proto.getSellerPayoutAmountFromMediation()); // nullable processModel.setTakeOfferFeeTxId(ProtoUtil.stringOrNullFromProto(proto.getTakeOfferFeeTxId())); @@ -211,6 +227,7 @@ public class ProcessModel implements Model, PersistablePayload { String paymentStartedMessageState = proto.getPaymentStartedMessageState().isEmpty() ? MessageState.UNDEFINED.name() : proto.getPaymentStartedMessageState(); ObjectProperty paymentStartedMessageStateProperty = processModel.getPaymentStartedMessageStateProperty(); paymentStartedMessageStateProperty.set(ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageState)); + processModel.setMediatedPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getMediatedPayoutTxSignature())); return processModel; } @@ -232,6 +249,7 @@ public class ProcessModel implements Model, PersistablePayload { AccountAgeWitnessService accountAgeWitnessService, TradeStatisticsManager tradeStatisticsManager, ArbitratorManager arbitratorManager, + MediatorManager mediatorManager, KeyRing keyRing, boolean useSavingsWallet, Coin fundsNeededForTrade) { @@ -247,6 +265,7 @@ public class ProcessModel implements Model, PersistablePayload { this.accountAgeWitnessService = accountAgeWitnessService; this.tradeStatisticsManager = tradeStatisticsManager; this.arbitratorManager = arbitratorManager; + this.mediatorManager = mediatorManager; this.keyRing = keyRing; this.p2PService = p2PService; this.useSavingsWallet = useSavingsWallet; diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java index e56beaff13..60894b2633 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java @@ -110,7 +110,9 @@ public class SellerAsMakerProtocol extends TradeProtocol implements SellerProtoc /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void handleTakeOfferRequest(TradeMessage tradeMessage, NodeAddress sender, ErrorMessageHandler errorMessageHandler) { + public void handleTakeOfferRequest(TradeMessage tradeMessage, + NodeAddress sender, + ErrorMessageHandler errorMessageHandler) { Validator.checkTradeId(processModel.getOfferId(), tradeMessage); checkArgument(tradeMessage instanceof PayDepositRequest); processModel.setTradeMessage(tradeMessage); @@ -146,7 +148,7 @@ public class SellerAsMakerProtocol extends TradeProtocol implements SellerProtoc // Incoming message handling /////////////////////////////////////////////////////////////////////////////////////////// - private void handle(DepositTxPublishedMessage tradeMessage, NodeAddress sender) { + protected void handle(DepositTxPublishedMessage tradeMessage, NodeAddress sender) { processModel.setTradeMessage(tradeMessage); processModel.setTempTradingPeerNodeAddress(sender); @@ -241,12 +243,15 @@ public class SellerAsMakerProtocol extends TradeProtocol implements SellerProtoc } } + /////////////////////////////////////////////////////////////////////////////////////////// // Massage dispatcher /////////////////////////////////////////////////////////////////////////////////////////// @Override protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress sender) { + super.doHandleDecryptedMessage(tradeMessage, sender); + log.info("Received {} from {} with tradeId {} and uid {}", tradeMessage.getClass().getSimpleName(), sender, tradeMessage.getTradeId(), tradeMessage.getUid()); @@ -254,8 +259,6 @@ public class SellerAsMakerProtocol extends TradeProtocol implements SellerProtoc handle((DepositTxPublishedMessage) tradeMessage, sender); } else if (tradeMessage instanceof CounterCurrencyTransferStartedMessage) { handle((CounterCurrencyTransferStartedMessage) tradeMessage, sender); - } else { - log.error("Incoming tradeMessage not supported. " + tradeMessage); } } } diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java index ce768c957a..d3071b1c8b 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java @@ -36,7 +36,6 @@ import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerSignAndPublis import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx; import bisq.core.trade.protocol.tasks.taker.TakerProcessPublishDepositTxRequest; import bisq.core.trade.protocol.tasks.taker.TakerPublishFeeTx; -import bisq.core.trade.protocol.tasks.taker.TakerSelectMediator; import bisq.core.trade.protocol.tasks.taker.TakerSendDepositTxPublishedMessage; import bisq.core.trade.protocol.tasks.taker.TakerSendPayDepositRequest; import bisq.core.trade.protocol.tasks.taker.TakerVerifyAndSignContract; @@ -108,7 +107,6 @@ public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtoc taskRunner.addTasks( TakerVerifyMakerAccount.class, TakerVerifyMakerFeePayment.class, - TakerSelectMediator.class, CreateTakerFeeTx.class, SellerAsTakerCreatesDepositTxInputs.class, TakerSendPayDepositRequest.class @@ -235,6 +233,8 @@ public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtoc @Override protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress sender) { + super.doHandleDecryptedMessage(tradeMessage, sender); + log.info("Received {} from {} with tradeId {} and uid {}", tradeMessage.getClass().getSimpleName(), sender, tradeMessage.getTradeId(), tradeMessage.getUid()); @@ -242,8 +242,6 @@ public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtoc handle((PublishDepositTxRequest) tradeMessage, sender); } else if (tradeMessage instanceof CounterCurrencyTransferStartedMessage) { handle((CounterCurrencyTransferStartedMessage) tradeMessage, sender); - } else { - log.error("Incoming message not supported. " + tradeMessage); } } } diff --git a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java index 6452382e3e..8862fb9fc1 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -21,8 +21,19 @@ import bisq.core.trade.MakerTrade; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; +import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; import bisq.core.trade.messages.PayDepositRequest; import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.protocol.tasks.ApplyFilter; +import bisq.core.trade.protocol.tasks.mediation.BroadcastMediatedPayoutTx; +import bisq.core.trade.protocol.tasks.mediation.FinalizeMediatedPayoutTx; +import bisq.core.trade.protocol.tasks.mediation.ProcessMediatedPayoutSignatureMessage; +import bisq.core.trade.protocol.tasks.mediation.ProcessMediatedPayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.mediation.SendMediatedPayoutSignatureMessage; +import bisq.core.trade.protocol.tasks.mediation.SendMediatedPayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.mediation.SetupMediatedPayoutTxListener; +import bisq.core.trade.protocol.tasks.mediation.SignMediatedPayoutTx; import bisq.network.p2p.AckMessage; import bisq.network.p2p.AckMessageSourceType; @@ -35,6 +46,8 @@ import bisq.network.p2p.SendMailboxMessageListener; import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.crypto.PubKeyRing; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; import bisq.common.proto.network.NetworkEnvelope; import javafx.beans.value.ChangeListener; @@ -102,10 +115,118 @@ public abstract class TradeProtocol { trade.stateProperty().addListener(stateChangeListener); } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Mediation: Called from UI if trader accepts mediation result + /////////////////////////////////////////////////////////////////////////////////////////// + + // Trader has not yet received the peer's signature but has clicked the accept button. + public void onAcceptMediationResult(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + if (trade.getProcessModel().getTradingPeer().getMediatedPayoutTxSignature() != null) { + errorMessageHandler.handleErrorMessage("We have received already the signature from the peer."); + return; + } + + TradeTaskRunner taskRunner = new TradeTaskRunner(trade, + () -> { + resultHandler.handleResult(); + handleTaskRunnerSuccess("onAcceptMediationResult"); + }, + (errorMessage) -> { + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(errorMessage); + }); + taskRunner.addTasks( + ApplyFilter.class, + SignMediatedPayoutTx.class, + SendMediatedPayoutSignatureMessage.class, + SetupMediatedPayoutTxListener.class + ); + taskRunner.run(); + } + + + // Trader has already received the peer's signature and has clicked the accept button as well. + public void onFinalizeMediationResultPayout(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + if (trade.getPayoutTx() != null) { + errorMessageHandler.handleErrorMessage("Payout tx is already published."); + return; + } + + TradeTaskRunner taskRunner = new TradeTaskRunner(trade, + () -> { + resultHandler.handleResult(); + handleTaskRunnerSuccess("onAcceptMediationResult"); + }, + (errorMessage) -> { + errorMessageHandler.handleErrorMessage(errorMessage); + handleTaskRunnerFault(errorMessage); + }); + taskRunner.addTasks( + ApplyFilter.class, + SignMediatedPayoutTx.class, + FinalizeMediatedPayoutTx.class, + BroadcastMediatedPayoutTx.class, + SendMediatedPayoutTxPublishedMessage.class + ); + taskRunner.run(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Mediation: incoming message + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void handle(MediatedPayoutTxSignatureMessage tradeMessage, NodeAddress sender) { + processModel.setTradeMessage(tradeMessage); + processModel.setTempTradingPeerNodeAddress(sender); + + TradeTaskRunner taskRunner = new TradeTaskRunner(trade, + () -> handleTaskRunnerSuccess(tradeMessage, "MediatedPayoutSignatureMessage"), + errorMessage -> handleTaskRunnerFault(tradeMessage, errorMessage)); + + taskRunner.addTasks( + ProcessMediatedPayoutSignatureMessage.class + ); + taskRunner.run(); + } + + protected void handle(MediatedPayoutTxPublishedMessage tradeMessage, NodeAddress sender) { + processModel.setTradeMessage(tradeMessage); + processModel.setTempTradingPeerNodeAddress(sender); + + TradeTaskRunner taskRunner = new TradeTaskRunner(trade, + () -> handleTaskRunnerSuccess(tradeMessage, "handle PayoutTxPublishedMessage"), + errorMessage -> handleTaskRunnerFault(tradeMessage, errorMessage)); + + taskRunner.addTasks( + ProcessMediatedPayoutTxPublishedMessage.class + ); + taskRunner.run(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Dispatcher + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress sender) { + if (tradeMessage instanceof MediatedPayoutTxSignatureMessage) { + handle((MediatedPayoutTxSignatureMessage) tradeMessage, sender); + } else if (tradeMessage instanceof MediatedPayoutTxPublishedMessage) { + handle((MediatedPayoutTxPublishedMessage) tradeMessage, sender); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + public void completed() { cleanup(); - // We only removed earlier the listner here, but then we migth have dangling trades after faults... + // We only removed earlier the listener here, but then we migth have dangling trades after faults... // so lets remove it at cleanup //processModel.getP2PService().removeDecryptedDirectMessageListener(decryptedDirectMessageListener); } @@ -131,8 +252,6 @@ public abstract class TradeProtocol { protected abstract void doApplyMailboxMessage(NetworkEnvelope networkEnvelope, Trade trade); - protected abstract void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress peerNodeAddress); - protected void startTimeout() { stopTimeout(); diff --git a/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java b/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java index 51c6a36cdf..94398caf79 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradingPeer.java @@ -71,6 +71,10 @@ public final class TradingPeer implements PersistablePayload { private byte[] accountAgeWitnessSignature; private long currentDate; + // Added in v.1.1.6 + @Nullable + private byte[] mediatedPayoutTxSignature; + public TradingPeer() { } @@ -90,6 +94,7 @@ public final class TradingPeer implements PersistablePayload { Optional.ofNullable(changeOutputAddress).ifPresent(builder::setChangeOutputAddress); Optional.ofNullable(accountAgeWitnessNonce).ifPresent(e -> builder.setAccountAgeWitnessNonce(ByteString.copyFrom(e))); Optional.ofNullable(accountAgeWitnessSignature).ifPresent(e -> builder.setAccountAgeWitnessSignature(ByteString.copyFrom(e))); + Optional.ofNullable(mediatedPayoutTxSignature).ifPresent(e -> builder.setMediatedPayoutTxSignature(ByteString.copyFrom(e))); builder.setCurrentDate(currentDate); return builder.build(); } @@ -118,6 +123,7 @@ public final class TradingPeer implements PersistablePayload { tradingPeer.setAccountAgeWitnessNonce(ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessNonce())); tradingPeer.setAccountAgeWitnessSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getAccountAgeWitnessSignature())); tradingPeer.setCurrentDate(proto.getCurrentDate()); + tradingPeer.setMediatedPayoutTxSignature(ProtoUtil.byteArrayOrNullFromProto(proto.getMediatedPayoutTxSignature())); return tradingPeer; } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java b/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java index 92c1185d95..07a223bde6 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/ApplyFilter.java @@ -32,7 +32,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class ApplyFilter extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public ApplyFilter(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/BroadcastPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/BroadcastPayoutTx.java new file mode 100644 index 0000000000..e74495d064 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/BroadcastPayoutTx.java @@ -0,0 +1,85 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.trade.protocol.tasks; + +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.trade.Trade; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public abstract class BroadcastPayoutTx extends TradeTask { + @SuppressWarnings({"unused"}) + public BroadcastPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + protected abstract void setState(); + + @Override + protected void run() { + try { + runInterceptHook(); + Transaction payoutTx = trade.getPayoutTx(); + checkNotNull(payoutTx, "payoutTx must not be null"); + + TransactionConfidence.ConfidenceType confidenceType = payoutTx.getConfidence().getConfidenceType(); + log.debug("payoutTx confidenceType:" + confidenceType); + if (confidenceType.equals(TransactionConfidence.ConfidenceType.BUILDING) || + confidenceType.equals(TransactionConfidence.ConfidenceType.PENDING)) { + log.debug("payoutTx was already published. confidenceType:" + confidenceType); + setState(); + complete(); + } else { + processModel.getTradeWalletService().broadcastTx(payoutTx, + new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + if (!completed) { + log.debug("BroadcastTx succeeded. Transaction:" + transaction); + setState(); + complete(); + } else { + log.warn("We got the onSuccess callback called after the timeout has been triggered a complete()."); + } + } + + @Override + public void onFailure(TxBroadcastException exception) { + if (!completed) { + log.error("BroadcastTx failed. Error:" + exception.getMessage()); + failed(exception); + } else { + log.warn("We got the onFailure callback called after the timeout has been triggered a complete()."); + } + } + }); + } + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/PublishTradeStatistics.java b/core/src/main/java/bisq/core/trade/protocol/tasks/PublishTradeStatistics.java index 48557033e8..9a51cbb8bf 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/PublishTradeStatistics.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/PublishTradeStatistics.java @@ -23,6 +23,8 @@ import bisq.core.trade.Trade; import bisq.core.trade.statistics.TradeStatistics2; import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.network.NetworkNode; +import bisq.network.p2p.network.TorNetworkNode; import bisq.common.taskrunner.TaskRunner; @@ -51,8 +53,14 @@ public class PublishTradeStatistics extends TradeTask { NodeAddress arbitratorNodeAddress = trade.getArbitratorNodeAddress(); if (arbitratorNodeAddress != null) { - // The first 4 chars are sufficient to identify an arbitrator - String address = arbitratorNodeAddress.getFullAddress().substring(0, 4); + + // The first 4 chars are sufficient to identify an arbitrator. + // For testing with regtest/localhost we use the full address as its localhost and would result in + // same values for multiple arbitrators. + NetworkNode networkNode = model.getProcessModel().getP2PService().getNetworkNode(); + String address = networkNode instanceof TorNetworkNode ? + arbitratorNodeAddress.getFullAddress().substring(0, 4) : + arbitratorNodeAddress.getFullAddress(); extraDataMap.put(TradeStatistics2.ARBITRATOR_ADDRESS, address); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SendPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SendPayoutTxPublishedMessage.java new file mode 100644 index 0000000000..c48aa3e5ff --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SendPayoutTxPublishedMessage.java @@ -0,0 +1,98 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.trade.protocol.tasks; + +import bisq.core.trade.Trade; +import bisq.core.trade.messages.TradeMessage; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendMailboxMessageListener; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class SendPayoutTxPublishedMessage extends TradeTask { + @SuppressWarnings({"unused"}) + public SendPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + protected abstract TradeMessage getMessage(String id); + + protected abstract void setStateSent(); + + protected abstract void setStateArrived(); + + protected abstract void setStateStoredInMailbox(); + + protected abstract void setStateFault(); + + @Override + protected void run() { + try { + runInterceptHook(); + if (trade.getPayoutTx() != null) { + String id = processModel.getOfferId(); + TradeMessage message = getMessage(id); + setStateSent(); + NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + + processModel.getP2PService().sendEncryptedMailboxMessage( + peersNodeAddress, + processModel.getTradingPeer().getPubKeyRing(), + message, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + setStateArrived(); + complete(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + setStateStoredInMailbox(); + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + setStateFault(); + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + failed(errorMessage); + } + } + ); + } else { + log.error("trade.getPayoutTx() = " + trade.getPayoutTx()); + failed("PayoutTx is null"); + } + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java new file mode 100644 index 0000000000..61dbe0ff53 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/SetupPayoutTxListener.java @@ -0,0 +1,124 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.trade.protocol.tasks; + +import bisq.core.btc.listeners.AddressConfidenceListener; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.trade.Trade; + +import bisq.common.UserThread; +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; + +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public abstract class SetupPayoutTxListener extends TradeTask { + // Use instance fields to not get eaten up by the GC + private Subscription tradeStateSubscription; + private AddressConfidenceListener confidenceListener; + + @SuppressWarnings({"unused"}) + public SetupPayoutTxListener(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + + protected abstract void setState(); + + @Override + protected void run() { + try { + runInterceptHook(); + if (!trade.isPayoutPublished()) { + BtcWalletService walletService = processModel.getBtcWalletService(); + final String id = processModel.getOffer().getId(); + Address address = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.TRADE_PAYOUT).getAddress(); + + final TransactionConfidence confidence = walletService.getConfidenceForAddress(address); + if (isInNetwork(confidence)) { + applyConfidence(confidence); + } else { + confidenceListener = new AddressConfidenceListener(address) { + @Override + public void onTransactionConfidenceChanged(TransactionConfidence confidence) { + if (isInNetwork(confidence)) + applyConfidence(confidence); + } + }; + walletService.addAddressConfidenceListener(confidenceListener); + + tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> { + if (trade.isPayoutPublished()) { + swapMultiSigEntry(); + + // hack to remove tradeStateSubscription at callback + UserThread.execute(this::unSubscribe); + } + }); + } + } + + // we complete immediately, our object stays alive because the balanceListener is stored in the WalletService + complete(); + } catch (Throwable t) { + failed(t); + } + } + + private void applyConfidence(TransactionConfidence confidence) { + if (trade.getPayoutTx() == null) { + Transaction walletTx = processModel.getTradeWalletService().getWalletTx(confidence.getTransactionHash()); + trade.setPayoutTx(walletTx); + BtcWalletService.printTx("payoutTx received from network", walletTx); + setState(); + } else { + log.info("We had the payout tx already set. tradeId={}, state={}", trade.getId(), trade.getState()); + } + + swapMultiSigEntry(); + + // need delay as it can be called inside the handler before the listener and tradeStateSubscription are actually set. + UserThread.execute(this::unSubscribe); + } + + private void swapMultiSigEntry() { + processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(trade.getId(), AddressEntry.Context.MULTI_SIG); + } + + private boolean isInNetwork(TransactionConfidence confidence) { + return confidence != null && + (confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.BUILDING) || + confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.PENDING)); + } + + private void unSubscribe() { + if (tradeStateSubscription != null) + tradeStateSubscription.unsubscribe(); + + if (confidenceListener != null) + processModel.getBtcWalletService().removeAddressConfidenceListener(confidenceListener); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/VerifyPeersAccountAgeWitness.java b/core/src/main/java/bisq/core/trade/protocol/tasks/VerifyPeersAccountAgeWitness.java index 2497f88939..2aa6e6ef17 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/VerifyPeersAccountAgeWitness.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/VerifyPeersAccountAgeWitness.java @@ -35,7 +35,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class VerifyPeersAccountAgeWitness extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public VerifyPeersAccountAgeWitness(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java index 3543fbddd1..6fc5f2d18c 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerProcessPayoutTxPublishedMessage.java @@ -35,7 +35,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class BuyerProcessPayoutTxPublishedMessage extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public BuyerProcessPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendCounterCurrencyTransferStartedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendCounterCurrencyTransferStartedMessage.java index f905981553..0dbf2ce3cd 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendCounterCurrencyTransferStartedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSendCounterCurrencyTransferStartedMessage.java @@ -34,7 +34,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class BuyerSendCounterCurrencyTransferStartedMessage extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public BuyerSendCounterCurrencyTransferStartedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupPayoutTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupPayoutTxListener.java index 178a08d786..db0dfff0cd 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupPayoutTxListener.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerSetupPayoutTxListener.java @@ -17,31 +17,16 @@ package bisq.core.trade.protocol.tasks.buyer; -import bisq.core.btc.listeners.AddressConfidenceListener; -import bisq.core.btc.model.AddressEntry; -import bisq.core.btc.wallet.BtcWalletService; import bisq.core.trade.Trade; -import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.trade.protocol.tasks.SetupPayoutTxListener; -import bisq.common.UserThread; import bisq.common.taskrunner.TaskRunner; -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionConfidence; - -import org.fxmisc.easybind.EasyBind; -import org.fxmisc.easybind.Subscription; - import lombok.extern.slf4j.Slf4j; @Slf4j -public class BuyerSetupPayoutTxListener extends TradeTask { - // Use instance fields to not get eaten up by the GC - private Subscription tradeStateSubscription; - private AddressConfidenceListener confidenceListener; - - @SuppressWarnings({"WeakerAccess", "unused"}) +public class BuyerSetupPayoutTxListener extends SetupPayoutTxListener { + @SuppressWarnings({"unused"}) public BuyerSetupPayoutTxListener(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -50,73 +35,17 @@ public class BuyerSetupPayoutTxListener extends TradeTask { protected void run() { try { runInterceptHook(); - if (!trade.isPayoutPublished()) { - BtcWalletService walletService = processModel.getBtcWalletService(); - final String id = processModel.getOffer().getId(); - Address address = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.TRADE_PAYOUT).getAddress(); - final TransactionConfidence confidence = walletService.getConfidenceForAddress(address); - if (isInNetwork(confidence)) { - applyConfidence(confidence); - } else { - confidenceListener = new AddressConfidenceListener(address) { - @Override - public void onTransactionConfidenceChanged(TransactionConfidence confidence) { - if (isInNetwork(confidence)) - applyConfidence(confidence); - } - }; - walletService.addAddressConfidenceListener(confidenceListener); + super.run(); - tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> { - if (trade.isPayoutPublished()) { - swapMultiSigEntry(); - - // hack to remove tradeStateSubscription at callback - UserThread.execute(this::unSubscribe); - } - }); - } - } - - // we complete immediately, our object stays alive because the balanceListener is stored in the WalletService - complete(); } catch (Throwable t) { failed(t); } } - private void applyConfidence(TransactionConfidence confidence) { - if (trade.getPayoutTx() == null) { - Transaction walletTx = processModel.getTradeWalletService().getWalletTx(confidence.getTransactionHash()); - trade.setPayoutTx(walletTx); - BtcWalletService.printTx("payoutTx received from network", walletTx); - trade.setState(Trade.State.BUYER_SAW_PAYOUT_TX_IN_NETWORK); - } else { - log.info("We got the payout tx already set from BuyerProcessPayoutTxPublishedMessage. tradeId={}, state={}", trade.getId(), trade.getState()); - } - - swapMultiSigEntry(); - - // need delay as it can be called inside the handler before the listener and tradeStateSubscription are actually set. - UserThread.execute(this::unSubscribe); + @Override + protected void setState() { + trade.setState(Trade.State.BUYER_SAW_PAYOUT_TX_IN_NETWORK); } - private void swapMultiSigEntry() { - processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(trade.getId(), AddressEntry.Context.MULTI_SIG); - } - - private boolean isInNetwork(TransactionConfidence confidence) { - return confidence != null && - (confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.BUILDING) || - confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.PENDING)); - } - - private void unSubscribe() { - if (tradeStateSubscription != null) - tradeStateSubscription.unsubscribe(); - - if (confidenceListener != null) - processModel.getBtcWalletService().removeAddressConfidenceListener(confidenceListener); - } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java index 4edd9bdb66..d49995560b 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerCreatesAndSignsDepositTx.java @@ -18,9 +18,9 @@ package bisq.core.trade.protocol.tasks.buyer_as_maker; import bisq.core.btc.model.AddressEntry; -import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.model.PreparedDepositTxAndMakerInputs; import bisq.core.btc.model.RawTransactionInput; +import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.Offer; import bisq.core.trade.Trade; import bisq.core.trade.protocol.TradingPeer; @@ -43,7 +43,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class BuyerAsMakerCreatesAndSignsDepositTx extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public BuyerAsMakerCreatesAndSignsDepositTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSignPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSignPayoutTx.java index b1f48b471e..4140ce364d 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSignPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_maker/BuyerAsMakerSignPayoutTx.java @@ -19,6 +19,7 @@ package bisq.core.trade.protocol.tasks.buyer_as_maker; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; import bisq.core.trade.Trade; import bisq.core.trade.protocol.tasks.TradeTask; @@ -34,11 +35,12 @@ import java.util.Arrays; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class BuyerAsMakerSignPayoutTx extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public BuyerAsMakerSignPayoutTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -49,12 +51,13 @@ public class BuyerAsMakerSignPayoutTx extends TradeTask { runInterceptHook(); Preconditions.checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); Preconditions.checkNotNull(trade.getDepositTx(), "trade.getDepositTx() must not be null"); + Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); BtcWalletService walletService = processModel.getBtcWalletService(); String id = processModel.getOffer().getId(); - Coin buyerPayoutAmount = trade.getOffer().getBuyerSecurityDeposit().add(trade.getTradeAmount()); - Coin sellerPayoutAmount = trade.getOffer().getSellerSecurityDeposit(); + Coin buyerPayoutAmount = offer.getBuyerSecurityDeposit().add(trade.getTradeAmount()); + Coin sellerPayoutAmount = offer.getSellerSecurityDeposit(); String buyerPayoutAddressString = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.TRADE_PAYOUT).getAddressString(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerCreatesDepositTxInputs.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerCreatesDepositTxInputs.java index 451a1d51a7..79b18574dd 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerCreatesDepositTxInputs.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerCreatesDepositTxInputs.java @@ -33,7 +33,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class BuyerAsTakerCreatesDepositTxInputs extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public BuyerAsTakerCreatesDepositTxInputs(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignAndPublishDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignAndPublishDepositTx.java index 0e4d4edcc1..125e347578 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignAndPublishDepositTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer_as_taker/BuyerAsTakerSignAndPublishDepositTx.java @@ -19,8 +19,8 @@ package bisq.core.trade.protocol.tasks.buyer_as_taker; import bisq.core.btc.exceptions.TxBroadcastException; import bisq.core.btc.model.AddressEntry; -import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.model.RawTransactionInput; +import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.trade.Trade; import bisq.core.trade.protocol.TradingPeer; @@ -44,7 +44,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class BuyerAsTakerSignAndPublishDepositTx extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public BuyerAsTakerSignAndPublishDepositTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerCreateAndSignContract.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerCreateAndSignContract.java index 043e8a5bab..417a4be432 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerCreateAndSignContract.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerCreateAndSignContract.java @@ -41,7 +41,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class MakerCreateAndSignContract extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public MakerCreateAndSignContract(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessDepositTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessDepositTxPublishedMessage.java index 5325934d98..c000c49e3f 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessDepositTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessDepositTxPublishedMessage.java @@ -35,7 +35,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class MakerProcessDepositTxPublishedMessage extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public MakerProcessDepositTxPublishedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessPayDepositRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessPayDepositRequest.java index ce72b92458..4a2cf510f7 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessPayDepositRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerProcessPayDepositRequest.java @@ -18,10 +18,16 @@ package bisq.core.trade.protocol.tasks.maker; import bisq.core.exceptions.TradePriceOutOfToleranceException; +import bisq.core.offer.Offer; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.core.trade.Trade; import bisq.core.trade.messages.PayDepositRequest; import bisq.core.trade.protocol.TradingPeer; import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.user.User; + +import bisq.network.p2p.NodeAddress; import bisq.common.taskrunner.TaskRunner; @@ -38,7 +44,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class MakerProcessPayDepositRequest extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public MakerProcessPayDepositRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -72,16 +78,34 @@ public class MakerProcessPayDepositRequest extends TradeTask { failed("acceptedArbitratorNodeAddresses must not be empty"); // Taker has to sign offerId (he cannot manipulate that - so we avoid to have a challenge protocol for passing the nonce we want to get signed) - tradingPeer.setAccountAgeWitnessNonce(trade.getOffer().getId().getBytes(Charsets.UTF_8)); + tradingPeer.setAccountAgeWitnessNonce(trade.getId().getBytes(Charsets.UTF_8)); tradingPeer.setAccountAgeWitnessSignature(payDepositRequest.getAccountAgeWitnessSignatureOfOfferId()); tradingPeer.setCurrentDate(payDepositRequest.getCurrentDate()); - trade.setArbitratorNodeAddress(checkNotNull(payDepositRequest.getArbitratorNodeAddress())); - trade.setMediatorNodeAddress(checkNotNull(payDepositRequest.getMediatorNodeAddress())); + User user = checkNotNull(processModel.getUser(), "User must not be null"); + NodeAddress arbitratorNodeAddress = checkNotNull(payDepositRequest.getArbitratorNodeAddress(), + "payDepositRequest.getArbitratorNodeAddress() must not be null"); + trade.setArbitratorNodeAddress(arbitratorNodeAddress); + Arbitrator arbitrator = checkNotNull(user.getAcceptedArbitratorByAddress(arbitratorNodeAddress), + "user.getAcceptedArbitratorByAddress(arbitratorNodeAddress) must not be null"); + trade.setArbitratorBtcPubKey(checkNotNull(arbitrator.getBtcPubKey(), + "arbitrator.getBtcPubKey() must not be null")); + trade.setArbitratorPubKeyRing(checkNotNull(arbitrator.getPubKeyRing(), + "arbitrator.getPubKeyRing() must not be null")); + + NodeAddress mediatorNodeAddress = checkNotNull(payDepositRequest.getMediatorNodeAddress(), + "payDepositRequest.getMediatorNodeAddress() must not be null"); + trade.setMediatorNodeAddress(mediatorNodeAddress); + Mediator mediator = checkNotNull(user.getAcceptedMediatorByAddress(mediatorNodeAddress), + "user.getAcceptedArbitratorByAddress(arbitratorNodeAddress) must not be null"); + trade.setMediatorPubKeyRing(checkNotNull(mediator.getPubKeyRing(), + "mediator.getPubKeyRing() must not be null")); + + Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null"); try { long takersTradePrice = payDepositRequest.getTradePrice(); - trade.getOffer().checkTradePriceTolerance(takersTradePrice); + offer.checkTradePriceTolerance(takersTradePrice); trade.setTradePrice(takersTradePrice); } catch (TradePriceOutOfToleranceException e) { failed(e.getMessage()); @@ -94,6 +118,8 @@ public class MakerProcessPayDepositRequest extends TradeTask { trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + trade.persist(); + processModel.removeMailboxMessageAfterProcessing(trade); complete(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendPublishDepositTxRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendPublishDepositTxRequest.java index 82e2dd1fe1..8bb029f140 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendPublishDepositTxRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSendPublishDepositTxRequest.java @@ -42,7 +42,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class MakerSendPublishDepositTxRequest extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public MakerSendPublishDepositTxRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetupDepositTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetupDepositTxListener.java index b0a2c02872..a858124c5a 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetupDepositTxListener.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerSetupDepositTxListener.java @@ -44,7 +44,7 @@ public class MakerSetupDepositTxListener extends TradeTask { private Subscription tradeStateSubscription; private AddressConfidenceListener confidenceListener; - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public MakerSetupDepositTxListener(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyTakerAccount.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyTakerAccount.java index 0f724d869c..2f24a9be97 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyTakerAccount.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyTakerAccount.java @@ -26,7 +26,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class MakerVerifyTakerAccount extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public MakerVerifyTakerAccount(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyTakerFeePayment.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyTakerFeePayment.java index b704cfc60b..8f30e97895 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyTakerFeePayment.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyTakerFeePayment.java @@ -27,7 +27,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class MakerVerifyTakerFeePayment extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public MakerVerifyTakerFeePayment(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyArbitratorSelection.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/BroadcastMediatedPayoutTx.java similarity index 54% rename from core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyArbitratorSelection.java rename to core/src/main/java/bisq/core/trade/protocol/tasks/mediation/BroadcastMediatedPayoutTx.java index 4e85e902aa..c1248792f6 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/MakerVerifyArbitratorSelection.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/BroadcastMediatedPayoutTx.java @@ -15,23 +15,20 @@ * along with Bisq. If not, see . */ -package bisq.core.trade.protocol.tasks.maker; +package bisq.core.trade.protocol.tasks.mediation; +import bisq.core.support.dispute.mediation.MediationResultState; import bisq.core.trade.Trade; -import bisq.core.trade.protocol.ArbitratorSelectionRule; -import bisq.core.trade.protocol.tasks.TradeTask; - -import bisq.network.p2p.NodeAddress; +import bisq.core.trade.protocol.tasks.BroadcastPayoutTx; import bisq.common.taskrunner.TaskRunner; import lombok.extern.slf4j.Slf4j; @Slf4j -public class MakerVerifyArbitratorSelection extends TradeTask { - - @SuppressWarnings({"WeakerAccess", "unused"}) - public MakerVerifyArbitratorSelection(TaskRunner taskHandler, Trade trade) { +public class BroadcastMediatedPayoutTx extends BroadcastPayoutTx { + @SuppressWarnings({"unused"}) + public BroadcastMediatedPayoutTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -40,16 +37,14 @@ public class MakerVerifyArbitratorSelection extends TradeTask { try { runInterceptHook(); - final NodeAddress selectedAddress = ArbitratorSelectionRule.select( - processModel.getTakerAcceptedArbitratorNodeAddresses(), - processModel.getOffer()); - if (trade.getArbitratorNodeAddress() != null && - trade.getArbitratorNodeAddress().equals(selectedAddress)) - complete(); - else - failed("Arbitrator selection verification failed"); + super.run(); } catch (Throwable t) { failed(t); } } + + @Override + protected void setState() { + trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED); + } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/FinalizeMediatedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/FinalizeMediatedPayoutTx.java new file mode 100644 index 0000000000..b88a39ff05 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/FinalizeMediatedPayoutTx.java @@ -0,0 +1,121 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class FinalizeMediatedPayoutTx extends TradeTask { + + @SuppressWarnings({"unused"}) + public FinalizeMediatedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + Transaction depositTx = checkNotNull(trade.getDepositTx()); + String tradeId = trade.getId(); + TradingPeer tradingPeer = processModel.getTradingPeer(); + BtcWalletService walletService = processModel.getBtcWalletService(); + Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); + Coin tradeAmount = checkNotNull(trade.getTradeAmount(), "tradeAmount must not be null"); + Contract contract = checkNotNull(trade.getContract(), "contract must not be null"); + + checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); + + + byte[] mySignature = checkNotNull(processModel.getMediatedPayoutTxSignature(), + "processModel.getTxSignatureFromMediation must not be null"); + byte[] peersSignature = checkNotNull(tradingPeer.getMediatedPayoutTxSignature(), + "tradingPeer.getTxSignatureFromMediation must not be null"); + + boolean isMyRoleBuyer = contract.isMyRoleBuyer(processModel.getPubKeyRing()); + byte[] buyerSignature = isMyRoleBuyer ? mySignature : peersSignature; + byte[] sellerSignature = isMyRoleBuyer ? peersSignature : mySignature; + + Coin totalPayoutAmount = offer.getBuyerSecurityDeposit().add(tradeAmount).add(offer.getSellerSecurityDeposit()); + Coin buyerPayoutAmount = Coin.valueOf(processModel.getBuyerPayoutAmountFromMediation()); + Coin sellerPayoutAmount = Coin.valueOf(processModel.getSellerPayoutAmountFromMediation()); + checkArgument(totalPayoutAmount.equals(buyerPayoutAmount.add(sellerPayoutAmount)), + "Payout amount does not match buyerPayoutAmount=" + buyerPayoutAmount.toFriendlyString() + + "; sellerPayoutAmount=" + sellerPayoutAmount); + + String myPayoutAddressString = walletService.getOrCreateAddressEntry(tradeId, AddressEntry.Context.TRADE_PAYOUT).getAddressString(); + String peersPayoutAddressString = tradingPeer.getPayoutAddressString(); + String buyerPayoutAddressString = isMyRoleBuyer ? myPayoutAddressString : peersPayoutAddressString; + String sellerPayoutAddressString = isMyRoleBuyer ? peersPayoutAddressString : myPayoutAddressString; + + byte[] myMultiSigPubKey = processModel.getMyMultiSigPubKey(); + byte[] peersMultiSigPubKey = tradingPeer.getMultiSigPubKey(); + byte[] buyerMultiSigPubKey = isMyRoleBuyer ? myMultiSigPubKey : peersMultiSigPubKey; + byte[] sellerMultiSigPubKey = isMyRoleBuyer ? peersMultiSigPubKey : myMultiSigPubKey; + + DeterministicKey multiSigKeyPair = walletService.getMultiSigKeyPair(tradeId, myMultiSigPubKey); + + checkArgument(Arrays.equals(myMultiSigPubKey, + walletService.getOrCreateAddressEntry(tradeId, AddressEntry.Context.MULTI_SIG).getPubKey()), + "myMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + tradeId); + + Transaction transaction = processModel.getTradeWalletService().finalizeMediatedPayoutTx( + depositTx, + buyerSignature, + sellerSignature, + buyerPayoutAmount, + sellerPayoutAmount, + buyerPayoutAddressString, + sellerPayoutAddressString, + multiSigKeyPair, + buyerMultiSigPubKey, + sellerMultiSigPubKey, + trade.getArbitratorBtcPubKey() + ); + + trade.setPayoutTx(transaction); + + walletService.swapTradeEntryToAvailableEntry(tradeId, AddressEntry.Context.MULTI_SIG); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} + diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutSignatureMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutSignatureMessage.java new file mode 100644 index 0000000000..0a26804bf8 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutSignatureMessage.java @@ -0,0 +1,61 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.util.Validator; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class ProcessMediatedPayoutSignatureMessage extends TradeTask { + @SuppressWarnings({"unused"}) + public ProcessMediatedPayoutSignatureMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + log.debug("current trade state " + trade.getState()); + MediatedPayoutTxSignatureMessage message = (MediatedPayoutTxSignatureMessage) processModel.getTradeMessage(); + Validator.checkTradeId(processModel.getOfferId(), message); + checkNotNull(message); + + processModel.getTradingPeer().setMediatedPayoutTxSignature(checkNotNull(message.getTxSignature())); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + processModel.removeMailboxMessageAfterProcessing(trade); + + trade.setMediationResultState(MediationResultState.RECEIVED_SIG_MSG); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutTxPublishedMessage.java new file mode 100644 index 0000000000..21344cf35b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/ProcessMediatedPayoutTxPublishedMessage.java @@ -0,0 +1,77 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; +import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.util.Validator; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class ProcessMediatedPayoutTxPublishedMessage extends TradeTask { + @SuppressWarnings({"unused"}) + public ProcessMediatedPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + MediatedPayoutTxPublishedMessage message = (MediatedPayoutTxPublishedMessage) processModel.getTradeMessage(); + Validator.checkTradeId(processModel.getOfferId(), message); + checkNotNull(message); + checkArgument(message.getPayoutTx() != null); + + // update to the latest peer address of our peer if the message is correct + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + + if (trade.getPayoutTx() == null) { + Transaction walletTx = processModel.getTradeWalletService().addTxToWallet(message.getPayoutTx()); + trade.setPayoutTx(walletTx); + BtcWalletService.printTx("payoutTx received from peer", walletTx); + + trade.setMediationResultState(MediationResultState.RECEIVED_PAYOUT_TX_PUBLISHED_MSG); + + if (trade.getPayoutTx() != null) { + processModel.getTradeManager().closeDisputedTrade(trade.getId(), Trade.DisputeState.MEDIATION_CLOSED); + } + + processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(trade.getId(), AddressEntry.Context.MULTI_SIG); + } else { + log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId()); + } + processModel.removeMailboxMessageAfterProcessing(trade); + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutSignatureMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutSignatureMessage.java new file mode 100644 index 0000000000..6289c6d272 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutSignatureMessage.java @@ -0,0 +1,101 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; +import bisq.network.p2p.SendMailboxMessageListener; + +import bisq.common.crypto.PubKeyRing; +import bisq.common.taskrunner.TaskRunner; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SendMediatedPayoutSignatureMessage extends TradeTask { + @SuppressWarnings({"unused"}) + public SendMediatedPayoutSignatureMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + PubKeyRing pubKeyRing = processModel.getPubKeyRing(); + Contract contract = checkNotNull(trade.getContract(), "contract must not be null"); + PubKeyRing peersPubKeyRing = contract.getPeersPubKeyRing(pubKeyRing); + NodeAddress peersNodeAddress = contract.getPeersNodeAddress(pubKeyRing); + log.error("sendBuyerSendPayoutSignatureMessage to peerAddress " + peersNodeAddress); + P2PService p2PService = processModel.getP2PService(); + MediatedPayoutTxSignatureMessage message = new MediatedPayoutTxSignatureMessage(processModel.getMediatedPayoutTxSignature(), + trade.getId(), + p2PService.getAddress(), + UUID.randomUUID().toString()); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + + trade.setMediationResultState(MediationResultState.SIG_MSG_SENT); + p2PService.sendEncryptedMailboxMessage(peersNodeAddress, + peersPubKeyRing, + message, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + + trade.setMediationResultState(MediationResultState.SIG_MSG_ARRIVED); + complete(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + + trade.setMediationResultState(MediationResultState.SIG_MSG_IN_MAILBOX); + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); + trade.setMediationResultState(MediationResultState.SIG_MSG_SEND_FAILED); + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); + failed(errorMessage); + } + } + ); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutTxPublishedMessage.java new file mode 100644 index 0000000000..b97e782d09 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SendMediatedPayoutTxPublishedMessage.java @@ -0,0 +1,85 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage; +import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.protocol.tasks.SendPayoutTxPublishedMessage; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Transaction; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + + +@Slf4j +public class SendMediatedPayoutTxPublishedMessage extends SendPayoutTxPublishedMessage { + @SuppressWarnings({"unused"}) + public SendMediatedPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected TradeMessage getMessage(String id) { + Transaction payoutTx = checkNotNull(trade.getPayoutTx(), "trade.getPayoutTx() must not be null"); + return new MediatedPayoutTxPublishedMessage( + id, + payoutTx.bitcoinSerialize(), + processModel.getMyNodeAddress(), + UUID.randomUUID().toString() + ); + } + + @Override + protected void setStateSent() { + trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED_MSG_SENT); + } + + @Override + protected void setStateArrived() { + trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED_MSG_ARRIVED); + } + + @Override + protected void setStateStoredInMailbox() { + trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED_MSG_IN_MAILBOX); + } + + @Override + protected void setStateFault() { + trade.setMediationResultState(MediationResultState.PAYOUT_TX_PUBLISHED_MSG_SEND_FAILED); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + super.run(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SetupMediatedPayoutTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SetupMediatedPayoutTxListener.java new file mode 100644 index 0000000000..716f2d7647 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SetupMediatedPayoutTxListener.java @@ -0,0 +1,54 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.support.dispute.mediation.MediationResultState; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.SetupPayoutTxListener; + +import bisq.common.taskrunner.TaskRunner; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class SetupMediatedPayoutTxListener extends SetupPayoutTxListener { + @SuppressWarnings({"unused"}) + public SetupMediatedPayoutTxListener(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + super.run(); + + } catch (Throwable t) { + failed(t); + } + } + + @Override + protected void setState() { + trade.setMediationResultState(MediationResultState.PAYOUT_TX_SEEN_IN_NETWORK); + if (trade.getPayoutTx() != null) { + processModel.getTradeManager().closeDisputedTrade(trade.getId(), Trade.DisputeState.MEDIATION_CLOSED); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SignMediatedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SignMediatedPayoutTx.java new file mode 100644 index 0000000000..59c27e8317 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/mediation/SignMediatedPayoutTx.java @@ -0,0 +1,110 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.core.trade.protocol.tasks.mediation; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.Offer; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.TradingPeer; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; + +import java.util.Arrays; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class SignMediatedPayoutTx extends TradeTask { + + @SuppressWarnings({"unused"}) + public SignMediatedPayoutTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + TradingPeer tradingPeer = processModel.getTradingPeer(); + if (processModel.getMediatedPayoutTxSignature() != null) { + log.warn("processModel.getTxSignatureFromMediation is already set"); + } + + String tradeId = trade.getId(); + BtcWalletService walletService = processModel.getBtcWalletService(); + Transaction depositTx = checkNotNull(trade.getDepositTx(), "trade.getDepositTx() must not be null"); + Offer offer = checkNotNull(trade.getOffer(), "offer must not be null"); + Coin tradeAmount = checkNotNull(trade.getTradeAmount(), "tradeAmount must not be null"); + Contract contract = checkNotNull(trade.getContract(), "contract must not be null"); + + Coin totalPayoutAmount = offer.getBuyerSecurityDeposit().add(tradeAmount).add(offer.getSellerSecurityDeposit()); + Coin buyerPayoutAmount = Coin.valueOf(processModel.getBuyerPayoutAmountFromMediation()); + Coin sellerPayoutAmount = Coin.valueOf(processModel.getSellerPayoutAmountFromMediation()); + + checkArgument(totalPayoutAmount.equals(buyerPayoutAmount.add(sellerPayoutAmount)), + "Payout amount does not match buyerPayoutAmount=" + buyerPayoutAmount.toFriendlyString() + + "; sellerPayoutAmount=" + sellerPayoutAmount); + + boolean isMyRoleBuyer = contract.isMyRoleBuyer(processModel.getPubKeyRing()); + + String myPayoutAddressString = walletService.getOrCreateAddressEntry(tradeId, AddressEntry.Context.TRADE_PAYOUT).getAddressString(); + String peersPayoutAddressString = tradingPeer.getPayoutAddressString(); + String buyerPayoutAddressString = isMyRoleBuyer ? myPayoutAddressString : peersPayoutAddressString; + String sellerPayoutAddressString = isMyRoleBuyer ? peersPayoutAddressString : myPayoutAddressString; + + byte[] myMultiSigPubKey = processModel.getMyMultiSigPubKey(); + byte[] peersMultiSigPubKey = tradingPeer.getMultiSigPubKey(); + byte[] buyerMultiSigPubKey = isMyRoleBuyer ? myMultiSigPubKey : peersMultiSigPubKey; + byte[] sellerMultiSigPubKey = isMyRoleBuyer ? peersMultiSigPubKey : myMultiSigPubKey; + + DeterministicKey myMultiSigKeyPair = walletService.getMultiSigKeyPair(tradeId, myMultiSigPubKey); + + checkArgument(Arrays.equals(myMultiSigPubKey, + walletService.getOrCreateAddressEntry(tradeId, AddressEntry.Context.MULTI_SIG).getPubKey()), + "myMultiSigPubKey from AddressEntry must match the one from the trade data. trade id =" + tradeId); + + byte[] mediatedPayoutTxSignature = processModel.getTradeWalletService().signMediatedPayoutTx( + depositTx, + buyerPayoutAmount, + sellerPayoutAmount, + buyerPayoutAddressString, + sellerPayoutAddressString, + myMultiSigKeyPair, + buyerMultiSigPubKey, + sellerMultiSigPubKey, + trade.getArbitratorBtcPubKey()); + processModel.setMediatedPayoutTxSignature(mediatedPayoutTxSignature); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} + diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerBroadcastPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerBroadcastPayoutTx.java index c056b52dd8..c27072538a 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerBroadcastPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerBroadcastPayoutTx.java @@ -17,23 +17,16 @@ package bisq.core.trade.protocol.tasks.seller; -import bisq.core.btc.exceptions.TxBroadcastException; -import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.trade.Trade; -import bisq.core.trade.protocol.tasks.TradeTask; +import bisq.core.trade.protocol.tasks.BroadcastPayoutTx; import bisq.common.taskrunner.TaskRunner; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionConfidence; - import lombok.extern.slf4j.Slf4j; -import static com.google.common.base.Preconditions.checkNotNull; - @Slf4j -public class SellerBroadcastPayoutTx extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) +public class SellerBroadcastPayoutTx extends BroadcastPayoutTx { + @SuppressWarnings({"unused"}) public SellerBroadcastPayoutTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -42,43 +35,15 @@ public class SellerBroadcastPayoutTx extends TradeTask { protected void run() { try { runInterceptHook(); - Transaction payoutTx = trade.getPayoutTx(); - checkNotNull(payoutTx, "payoutTx must not be null"); - TransactionConfidence.ConfidenceType confidenceType = payoutTx.getConfidence().getConfidenceType(); - log.debug("payoutTx confidenceType:" + confidenceType); - if (confidenceType.equals(TransactionConfidence.ConfidenceType.BUILDING) || - confidenceType.equals(TransactionConfidence.ConfidenceType.PENDING)) { - log.debug("payoutTx was already published. confidenceType:" + confidenceType); - trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX); - complete(); - } else { - processModel.getTradeWalletService().broadcastTx(payoutTx, - new TxBroadcaster.Callback() { - @Override - public void onSuccess(Transaction transaction) { - if (!completed) { - log.debug("BroadcastTx succeeded. Transaction:" + transaction); - trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX); - complete(); - } else { - log.warn("We got the onSuccess callback called after the timeout has been triggered a complete()."); - } - } - - @Override - public void onFailure(TxBroadcastException exception) { - if (!completed) { - log.error("BroadcastTx failed. Error:" + exception.getMessage()); - failed(exception); - } else { - log.warn("We got the onFailure callback called after the timeout has been triggered a complete()."); - } - } - }); - } + super.run(); } catch (Throwable t) { failed(t); } } + + @Override + protected void setState() { + trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX); + } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java index ab8cedc2d6..935d780c76 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerProcessCounterCurrencyTransferStartedMessage.java @@ -30,7 +30,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class SellerProcessCounterCurrencyTransferStartedMessage extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public SellerProcessCounterCurrencyTransferStartedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendPayoutTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendPayoutTxPublishedMessage.java index b277caec3f..c8af73c732 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendPayoutTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSendPayoutTxPublishedMessage.java @@ -19,76 +19,63 @@ package bisq.core.trade.protocol.tasks.seller; import bisq.core.trade.Trade; import bisq.core.trade.messages.PayoutTxPublishedMessage; -import bisq.core.trade.protocol.tasks.TradeTask; - -import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.SendMailboxMessageListener; +import bisq.core.trade.messages.TradeMessage; +import bisq.core.trade.protocol.tasks.SendPayoutTxPublishedMessage; import bisq.common.taskrunner.TaskRunner; +import org.bitcoinj.core.Transaction; + import java.util.UUID; import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkNotNull; + @Slf4j -public class SellerSendPayoutTxPublishedMessage extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) +public class SellerSendPayoutTxPublishedMessage extends SendPayoutTxPublishedMessage { + @SuppressWarnings({"unused"}) public SellerSendPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } + @Override + protected TradeMessage getMessage(String id) { + Transaction payoutTx = checkNotNull(trade.getPayoutTx(), "trade.getPayoutTx() must not be null"); + return new PayoutTxPublishedMessage( + id, + payoutTx.bitcoinSerialize(), + processModel.getMyNodeAddress(), + UUID.randomUUID().toString() + ); + } + + @Override + protected void setStateSent() { + trade.setState(Trade.State.SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG); + } + + @Override + protected void setStateArrived() { + trade.setState(Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG); + } + + @Override + protected void setStateStoredInMailbox() { + trade.setState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG); + } + + @Override + protected void setStateFault() { + trade.setState(Trade.State.SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG); + } + @Override protected void run() { try { runInterceptHook(); - if (trade.getPayoutTx() != null) { - final String id = processModel.getOfferId(); - final PayoutTxPublishedMessage message = new PayoutTxPublishedMessage( - id, - trade.getPayoutTx().bitcoinSerialize(), - processModel.getMyNodeAddress(), - UUID.randomUUID().toString() - ); - trade.setState(Trade.State.SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG); - NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); - log.info("Send {} to peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - processModel.getP2PService().sendEncryptedMailboxMessage( - peersNodeAddress, - processModel.getTradingPeer().getPubKeyRing(), - message, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - trade.setState(Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG); - complete(); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - trade.setState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG); - complete(); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); - trade.setState(Trade.State.SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG); - appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage); - failed(errorMessage); - } - } - ); - } else { - log.error("trade.getPayoutTx() = " + trade.getPayoutTx()); - failed("PayoutTx is null"); - } + super.run(); } catch (Throwable t) { failed(t); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndFinalizePayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndFinalizePayoutTx.java index 2648a14f99..80b3e4c634 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndFinalizePayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerSignAndFinalizePayoutTx.java @@ -41,7 +41,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class SellerSignAndFinalizePayoutTx extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public SellerSignAndFinalizePayoutTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerVerifiesPeersAccountAge.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerVerifiesPeersAccountAge.java index 78591b641a..a84b72aa64 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerVerifiesPeersAccountAge.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller/SellerVerifiesPeersAccountAge.java @@ -29,7 +29,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SellerVerifiesPeersAccountAge extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public SellerVerifiesPeersAccountAge(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesAndSignsDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesAndSignsDepositTx.java index 5cf0c2c3d7..28f3005163 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesAndSignsDepositTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_maker/SellerAsMakerCreatesAndSignsDepositTx.java @@ -18,9 +18,9 @@ package bisq.core.trade.protocol.tasks.seller_as_maker; import bisq.core.btc.model.AddressEntry; -import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.model.PreparedDepositTxAndMakerInputs; import bisq.core.btc.model.RawTransactionInput; +import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.Offer; import bisq.core.trade.Trade; import bisq.core.trade.protocol.TradingPeer; @@ -43,7 +43,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class SellerAsMakerCreatesAndSignsDepositTx extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public SellerAsMakerCreatesAndSignsDepositTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerCreatesDepositTxInputs.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerCreatesDepositTxInputs.java index 72f2d5791b..6298250566 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerCreatesDepositTxInputs.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerCreatesDepositTxInputs.java @@ -32,7 +32,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class SellerAsTakerCreatesDepositTxInputs extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public SellerAsTakerCreatesDepositTxInputs(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignAndPublishDepositTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignAndPublishDepositTx.java index 73709a364e..9bce294410 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignAndPublishDepositTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/seller_as_taker/SellerAsTakerSignAndPublishDepositTx.java @@ -19,8 +19,8 @@ package bisq.core.trade.protocol.tasks.seller_as_taker; import bisq.core.btc.exceptions.TxBroadcastException; import bisq.core.btc.model.AddressEntry; -import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.model.RawTransactionInput; +import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.trade.Contract; import bisq.core.trade.Trade; @@ -44,7 +44,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class SellerAsTakerSignAndPublishDepositTx extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public SellerAsTakerSignAndPublishDepositTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/CreateTakerFeeTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/CreateTakerFeeTx.java index 6a7ccfc846..1b9241f840 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/CreateTakerFeeTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/CreateTakerFeeTx.java @@ -17,13 +17,13 @@ package bisq.core.trade.protocol.tasks.taker; -import bisq.core.arbitration.Arbitrator; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.WalletService; import bisq.core.dao.exceptions.DaoDisabledException; -import bisq.core.offer.availability.ArbitratorSelection; +import bisq.core.offer.availability.DisputeAgentSelection; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.trade.Trade; import bisq.core.trade.protocol.tasks.TradeTask; @@ -37,7 +37,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class CreateTakerFeeTx extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public CreateTakerFeeTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -47,7 +47,7 @@ public class CreateTakerFeeTx extends TradeTask { try { runInterceptHook(); - Arbitrator arbitrator = ArbitratorSelection.getLeastUsedArbitrator(processModel.getTradeStatisticsManager(), + Arbitrator arbitrator = DisputeAgentSelection.getLeastUsedArbitrator(processModel.getTradeStatisticsManager(), processModel.getArbitratorManager()); BtcWalletService walletService = processModel.getBtcWalletService(); String id = processModel.getOffer().getId(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessPublishDepositTxRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessPublishDepositTxRequest.java index bd6c0bc4af..0160397d04 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessPublishDepositTxRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerProcessPublishDepositTxRequest.java @@ -33,7 +33,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class TakerProcessPublishDepositTxRequest extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public TakerProcessPublishDepositTxRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerPublishFeeTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerPublishFeeTx.java index 0325bbb8c3..1a2090cac4 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerPublishFeeTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerPublishFeeTx.java @@ -35,7 +35,7 @@ import javax.annotation.Nullable; @Slf4j public class TakerPublishFeeTx extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public TakerPublishFeeTx(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSelectMediator.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSelectMediator.java deleted file mode 100644 index e6e5dc92da..0000000000 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSelectMediator.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - -package bisq.core.trade.protocol.tasks.taker; - -import bisq.core.trade.Trade; -import bisq.core.trade.protocol.MediatorSelectionRule; -import bisq.core.trade.protocol.tasks.TradeTask; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.taskrunner.TaskRunner; - -import java.util.List; - -import lombok.extern.slf4j.Slf4j; - -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; - -@Slf4j -public class TakerSelectMediator extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) - public TakerSelectMediator(TaskRunner taskHandler, Trade trade) { - super(taskHandler, trade); - } - - @Override - protected void run() { - try { - runInterceptHook(); - List acceptedMediatorAddresses = processModel.getUser().getAcceptedMediatorAddresses(); - checkNotNull(acceptedMediatorAddresses, "acceptedMediatorAddresses must not be null"); - checkArgument(!acceptedMediatorAddresses.isEmpty(), "acceptedMediatorAddresses must not be empty"); - NodeAddress mediatorNodeAddress; - try { - mediatorNodeAddress = MediatorSelectionRule.select(acceptedMediatorAddresses, processModel.getOffer()); - } catch (Throwable t) { - // In case the mediator from the offer is not available anymore we just use the first. - // Mediators are not implemented anyway and we will remove it with the new trade protocol but we - // still need to be backward compatible so we cannot remove it now. - mediatorNodeAddress = acceptedMediatorAddresses.get(0); - } - - trade.setMediatorNodeAddress(mediatorNodeAddress); - complete(); - } catch (Throwable t) { - failed(t); - } - } -} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendDepositTxPublishedMessage.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendDepositTxPublishedMessage.java index e720f4354b..ce9034b98d 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendDepositTxPublishedMessage.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendDepositTxPublishedMessage.java @@ -32,7 +32,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class TakerSendDepositTxPublishedMessage extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public TakerSendDepositTxPublishedMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendPayDepositRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendPayDepositRequest.java index 6a35a72813..58f8591d47 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendPayDepositRequest.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerSendPayDepositRequest.java @@ -46,7 +46,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class TakerSendPayDepositRequest extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public TakerSendPayDepositRequest(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } @@ -73,7 +73,6 @@ public class TakerSendPayDepositRequest extends TradeTask { byte[] takerMultiSigPubKey = addressEntry.getPubKey(); processModel.setMyMultiSigPubKey(takerMultiSigPubKey); - checkArgument(walletService.getAddressEntry(id, AddressEntry.Context.TRADE_PAYOUT).isPresent(), "TRADE_PAYOUT addressEntry must have been already set here."); AddressEntry takerPayoutAddressEntry = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.TRADE_PAYOUT); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyAndSignContract.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyAndSignContract.java index 38430830f7..879a9ac6e9 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyAndSignContract.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyAndSignContract.java @@ -43,7 +43,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j public class TakerVerifyAndSignContract extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public TakerVerifyAndSignContract(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyMakerAccount.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyMakerAccount.java index 06420e5b89..417e94c5d8 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyMakerAccount.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyMakerAccount.java @@ -27,7 +27,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class TakerVerifyMakerAccount extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public TakerVerifyMakerAccount(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyMakerFeePayment.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyMakerFeePayment.java index c448d257f0..42971acccb 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyMakerFeePayment.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/TakerVerifyMakerFeePayment.java @@ -26,7 +26,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j public class TakerVerifyMakerFeePayment extends TradeTask { - @SuppressWarnings({"WeakerAccess", "unused"}) + @SuppressWarnings({"unused"}) public TakerVerifyMakerFeePayment(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); } diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java index 569b76cbf3..1210de64c8 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatistics2.java @@ -62,6 +62,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Value public final class TradeStatistics2 implements LazyProcessedPayload, PersistableNetworkPayload, PersistableEnvelope { public static final String ARBITRATOR_ADDRESS = "arbAddr"; + public static final String MEDIATOR_ADDRESS = "medAddr"; private final OfferPayload.Direction direction; private final String baseCurrency; diff --git a/core/src/main/java/bisq/core/user/User.java b/core/src/main/java/bisq/core/user/User.java index af8637efaa..d197a802c9 100644 --- a/core/src/main/java/bisq/core/user/User.java +++ b/core/src/main/java/bisq/core/user/User.java @@ -18,14 +18,14 @@ package bisq.core.user; import bisq.core.alert.Alert; -import bisq.core.arbitration.Arbitrator; -import bisq.core.arbitration.Mediator; import bisq.core.filter.Filter; import bisq.core.locale.LanguageUtil; import bisq.core.locale.TradeCurrency; import bisq.core.notifications.alerts.market.MarketAlertFilter; import bisq.core.notifications.alerts.price.PriceAlertFilter; import bisq.core.payment.PaymentAccount; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.network.p2p.NodeAddress; @@ -411,6 +411,14 @@ public class User implements PersistedDataHost { return userPayload.getAcceptedMediators(); } + public boolean hasAcceptedArbitrators() { + return getAcceptedArbitrators() != null && !getAcceptedArbitrators().isEmpty(); + } + + public boolean hasAcceptedMediators() { + return getAcceptedMediators() != null && !getAcceptedMediators().isEmpty(); + } + @Nullable public List getAcceptedMediatorAddresses() { return userPayload.getAcceptedMediators() != null ? userPayload.getAcceptedMediators().stream().map(Mediator::getNodeAddress).collect(Collectors.toList()) : null; diff --git a/core/src/main/java/bisq/core/user/UserPayload.java b/core/src/main/java/bisq/core/user/UserPayload.java index 8317f1cd01..878a711df9 100644 --- a/core/src/main/java/bisq/core/user/UserPayload.java +++ b/core/src/main/java/bisq/core/user/UserPayload.java @@ -18,13 +18,13 @@ package bisq.core.user; import bisq.core.alert.Alert; -import bisq.core.arbitration.Arbitrator; -import bisq.core.arbitration.Mediator; import bisq.core.filter.Filter; import bisq.core.notifications.alerts.market.MarketAlertFilter; import bisq.core.notifications.alerts.price.PriceAlertFilter; import bisq.core.payment.PaymentAccount; import bisq.core.proto.CoreProtoResolver; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.common.proto.ProtoUtil; import bisq.common.proto.persistable.PersistableEnvelope; @@ -95,7 +95,7 @@ public class UserPayload implements PersistableEnvelope { Optional.ofNullable(registeredArbitrator) .ifPresent(registeredArbitrator -> builder.setRegisteredArbitrator(registeredArbitrator.toProtoMessage().getArbitrator())); Optional.ofNullable(registeredMediator) - .ifPresent(developersAlert -> builder.setDevelopersAlert(developersAlert.toProtoMessage().getAlert())); + .ifPresent(registeredMediator -> builder.setRegisteredMediator(registeredMediator.toProtoMessage().getMediator())); Optional.ofNullable(acceptedArbitrators) .ifPresent(e -> builder.addAllAcceptedArbitrators(ProtoUtil.collectionToProto(acceptedArbitrators, message -> ((protobuf.StoragePayload) message).getArbitrator()))); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index fb06971842..2c4c392aca 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -119,7 +119,6 @@ shared.sendingConfirmationAgain=Please send confirmation again shared.exportCSV=Export to csv shared.exportJSON=Export to JSON shared.noDateAvailable=No date available -shared.arbitratorsFee=Arbitrator's fee shared.noDetailsAvailable=No details available shared.notUsedYet=Not used yet shared.date=Date @@ -207,6 +206,10 @@ shared.proposal=Proposal shared.votes=Votes shared.learnMore=Learn more shared.dismiss=Dismiss +shared.selectedArbitrator=Selected arbitrator +shared.selectedMediator=Selected mediator +shared.mediator=Mediator +shared.arbitrator2=Arbitrator #################################################################### # UI views @@ -541,8 +544,9 @@ portfolio.pending.step3_seller.confirmPaymentReceived=Confirm payment received portfolio.pending.step5.completed=Completed portfolio.pending.step1.info=Deposit transaction has been published.\n{0} need to wait for at least one blockchain confirmation before starting the payment. -portfolio.pending.step1.warn=The deposit transaction still did not get confirmed.\nThat might happen in rare cases when the funding fee of one trader from the external wallet was too low. -portfolio.pending.step1.openForDispute=The deposit transaction still did not get confirmed.\nThat might happen in rare cases when the funding fee of one trader from the external wallet was too low.\nThe max. period for the trade has elapsed.\n\nYou can wait longer or contact the arbitrator for opening a dispute. +portfolio.pending.step1.warn=The deposit transaction is still not confirmed. This sometimes happens in rare cases when the funding fee of one trader from an external wallet was too low. +portfolio.pending.step1.openForDispute=The deposit transaction is still not confirmed. \ + You can wait longer or contact the mediator for assistance. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2.confReached=Your trade has reached at least one blockchain confirmation.\n(You can wait for more confirmations if you want - 6 confirmations are considered as very secure.)\n\n @@ -560,9 +564,11 @@ portfolio.pending.step2_buyer.altcoin=Please transfer from your external {0} wal # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.cash=Please go to a bank and pay {0} to the BTC seller.\n\n portfolio.pending.step2_buyer.cash.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment write on the paper receipt: NO REFUNDS.\nThen tear it in 2 parts, make a photo and send it to the BTC seller's email address. +# suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.moneyGram=Please pay {0} to the BTC seller by using MoneyGram.\n\n portfolio.pending.step2_buyer.moneyGram.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the Authorisation number and a photo of the receipt by email to the BTC seller.\n\ The receipt must clearly show the seller''s full name, country, state and the amount. The seller''s email is: {0}. +# suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.westernUnion=Please pay {0} to the BTC seller by using Western Union.\n\n portfolio.pending.step2_buyer.westernUnion.extra=IMPORTANT REQUIREMENT:\nAfter you have done the payment send the MTCN (tracking number) and a photo of the receipt by email to the BTC seller.\n\ The receipt must clearly show the seller''s full name, city, country and the amount. The seller''s email is: {0}. @@ -571,14 +577,16 @@ portfolio.pending.step2_buyer.westernUnion.extra=IMPORTANT REQUIREMENT:\nAfter y portfolio.pending.step2_buyer.postal=Please send {0} by \"US Postal Money Order\" to the BTC seller.\n\n # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.bank=Please go to your online banking web page and pay {0} to the BTC seller.\n\n +# suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Please contact the BTC seller by the provided contact and arrange a meeting to pay {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Start payment using {0} portfolio.pending.step2_buyer.amountToTransfer=Amount to transfer portfolio.pending.step2_buyer.sellersAddress=Seller''s {0} address portfolio.pending.step2_buyer.buyerAccount=Your payment account to be used portfolio.pending.step2_buyer.paymentStarted=Payment started -portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1} otherwise the trade will be investigated by the arbitrator. -portfolio.pending.step2_buyer.openForDispute=You have not completed your payment!\nThe max. period for the trade has elapsed.\n\nPlease contact the arbitrator for opening a dispute. +portfolio.pending.step2_buyer.warn=You still have not done your {0} payment!\nPlease note that the trade has to be completed by {1}. +portfolio.pending.step2_buyer.openForDispute=You have not completed your payment!\nThe max. period for the trade has elapsed.\ + Please contact the mediator for assistance. portfolio.pending.step2_buyer.paperReceipt.headline=Did you send the paper receipt to the BTC seller? portfolio.pending.step2_buyer.paperReceipt.msg=Remember:\n\ You need to write on the paper receipt: NO REFUNDS.\n\ @@ -605,21 +613,21 @@ portfolio.pending.step2_buyer.confirmStart.yes=Yes, I have started the payment portfolio.pending.step2_seller.waitPayment.headline=Wait for payment portfolio.pending.step2_seller.f2fInfo.headline=Buyer's contact information portfolio.pending.step2_seller.waitPayment.msg=The deposit transaction has at least one blockchain confirmation.\nYou need to wait until the BTC buyer starts the {0} payment. -portfolio.pending.step2_seller.warn=The BTC buyer still has not done the {0} payment.\nYou need to wait until they have started the payment.\nIf the trade has not been completed on {1} the arbitrator will investigate. -portfolio.pending.step2_seller.openForDispute=The BTC buyer has not started his payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the arbitrator for opening a dispute. +portfolio.pending.step2_seller.warn=The BTC buyer still has not done the {0} payment.\nYou need to wait until they have started the payment.\nThe trade has to be completed by {1}. +portfolio.pending.step2_seller.openForDispute=The BTC buyer has not started their payment!\nThe max. allowed period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the mediator for assistance. tradeChat.chatWindowTitle=Chat window for trade with ID ''{0}'' tradeChat.openChat=Open chat window tradeChat.rules=You can communicate with your trade peer to resolve potential problems with this trade.\n\ It is not mandatory to reply in the chat.\n\ - If a trader violates the below rules, open a dispute with 'Cmd/Ctrl + o' and report it to the arbitrator.\n\n\ + If a trader violates any of the rules below, open a dispute and report it to the mediator or arbitrator.\n\n\ Chat rules:\n\ \t● Do not send any links (risk of malware). You can send the transaction ID and the name of a block explorer.\n\ \t● Do not send your seed words, private keys, passwords or other sensitive information!\n\ \t● Do not encourage trading outside of Bisq (no security).\n\ \t● Do not engage in any form of social engineering scam attempts.\n\ \t● If a peer is not responding and prefers to not communicate via chat, respect their decision.\n\ - \t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or trollbox.\n\ + \t● Keep conversation scope limited to the trade. This chat is not a messenger replacement or troll-box.\n\ \t● Keep conversation friendly and respectful. # suppress inspection "UnusedProperty" @@ -640,8 +648,10 @@ portfolio.pending.step3_buyer.wait.info=Waiting for the BTC seller''s confirmati portfolio.pending.step3_buyer.wait.msgStateInfo.label=Payment started message status portfolio.pending.step3_buyer.warn.part1a=on the {0} blockchain portfolio.pending.step3_buyer.warn.part1b=at your payment provider (e.g. bank) -portfolio.pending.step3_buyer.warn.part2=The BTC seller still has not confirmed your payment!\nPlease check {0} if the payment sending was successful.\nIf the BTC seller does not confirm the receipt of your payment by {1} the trade will be investigated by the arbitrator. -portfolio.pending.step3_buyer.openForDispute=The BTC seller has not confirmed your payment!\nThe max. period for the trade has elapsed.\nYou can wait longer and give the trading peer more time or contact the arbitrator for opening a dispute. +portfolio.pending.step3_buyer.warn.part2=The BTC seller still has not confirmed your payment. Please check {0} if the \ + payment sending was successful. +portfolio.pending.step3_buyer.openForDispute=The BTC seller has not confirmed your payment! The max. period for the \ + trade has elapsed. You can wait longer and give the trading peer more time or request assistance from the mediator. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.part=Your trading partner has confirmed that they have initiated the {0} payment.\n\n portfolio.pending.step3_seller.altcoin.explorer=on your favorite {0} blockchain explorer @@ -652,6 +662,7 @@ has already sufficient blockchain confirmations.\nThe payment amount has to be { You can copy & paste your {4} address from the main screen after closing that popup. portfolio.pending.step3_seller.postal={0}Please check if you have received {1} with \"US Postal Money Order\" from the BTC buyer.\n\n\ The trade ID (\"reason for payment\" text) of the transaction is: \"{2}\" +# suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.bank=Your trading partner has confirmed that they have initiated the {0} payment.\n\n\ Please go to your online banking web page and check if you have received {1} from the BTC buyer.\n\n\ The trade ID (\"reason for payment\" text) of the transaction is: \"{2}\"\n\n @@ -684,8 +695,10 @@ portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Check for blockchain portfolio.pending.step3_seller.buyerStartedPayment.fiat=Check at your trading account (e.g. bank account) and confirm when you have received the payment. portfolio.pending.step3_seller.warn.part1a=on the {0} blockchain portfolio.pending.step3_seller.warn.part1b=at your payment provider (e.g. bank) -portfolio.pending.step3_seller.warn.part2=You still have not confirmed the receipt of the payment!\nPlease check {0} if you have received the payment.\nIf you don''t confirm receipt by {1} the trade will be investigated by the arbitrator. -portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\nThe max. period for the trade has elapsed.\nPlease confirm or contact the arbitrator for opening a dispute. +portfolio.pending.step3_seller.warn.part2=You still have not confirmed the receipt of the payment. \ + Please check {0} if you have received the payment. +portfolio.pending.step3_seller.openForDispute=You have not confirmed the receipt of the payment!\n\ + The max. period for the trade has elapsed.\nPlease confirm or request assistance from the mediator. # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step3_seller.onPaymentReceived.part1=Have you received the {0} payment from your trading partner?\n\n # suppress inspection "TrailingSpacesInProperty" @@ -732,27 +745,70 @@ portfolio.pending.tradePeriodInfo=After the first blockchain confirmation, the t portfolio.pending.tradePeriodWarning=If the period is exceeded both traders can open a dispute. portfolio.pending.tradeNotCompleted=Trade not completed in time (until {0}) portfolio.pending.tradeProcess=Trade process -portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the arbitrator arrived (e.g. if you did not get a response after 1 day) feel free to open a dispute again. +portfolio.pending.openAgainDispute.msg=If you are not sure that the message to the mediator or arbitrator arrived \ + (e.g. if you did not get a response after 1 day) feel free to open a dispute again with Cmd/Ctrl+o. You can also ask \ + for additional help on the Bisq forum at https://bisq.community. portfolio.pending.openAgainDispute.button=Open dispute again portfolio.pending.openSupportTicket.headline=Open support ticket -portfolio.pending.openSupportTicket.msg=Please use that only in emergency case if you don't get displayed a \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and handled by the arbitrator +portfolio.pending.openSupportTicket.msg=Please use this function only in emergency cases if you don't see a \ + \"Open support\" or \"Open dispute\" button.\n\nWhen you open a support ticket the trade will be interrupted and \ + handled by a mediator or arbitrator. + portfolio.pending.notification=Notification -portfolio.pending.openDispute=Open a dispute -portfolio.pending.disputeOpened=Dispute opened + +portfolio.pending.support.headline.getHelp=Need help? +portfolio.pending.support.text.getHelp=If you have any problems you can try to contact the trade peer in the trade \ + chat or ask the Bisq community at https://bisq.community. \ + If your issue still isn't resolved, you can request more help from a mediator. +portfolio.pending.support.button.getHelp=Get support +portfolio.pending.support.popup.info=If your issue with the trade remains unsolved, you can open a support \ + ticket to request help from the mediator. If you have not received the payment, please wait until the trade period is over.\n\n\ + Are you sure you want to open a support ticket? +portfolio.pending.support.popup.button=Open support ticket +portfolio.pending.support.headline.halfPeriodOver=Check payment +portfolio.pending.support.headline.periodOver=Trade period is over + +portfolio.pending.arbitrationRequested=Arbitration requested +portfolio.pending.mediationRequested=Mediation requested portfolio.pending.openSupport=Open support ticket portfolio.pending.supportTicketOpened=Support ticket opened portfolio.pending.requestSupport=Request support -portfolio.pending.error.requestSupport=Please report the problem to your arbitrator.\n\nHe will forward the information to the developers to investigate the problem.\nAfter the problem has been analyzed you will get back all locked funds. +portfolio.pending.error.requestSupport=Please report the problem to your mediator or arbitrator.\n\nHe will forward the \ + information to the developers to investigate the problem.\nAfter the problem has been analyzed you will \ + get back all locked funds. portfolio.pending.communicateWithArbitrator=Please communicate in the \"Support\" screen with the arbitrator. +portfolio.pending.communicateWithMediator=Please communicate in the \"Support\" screen with the mediator. portfolio.pending.supportTicketOpenedMyUser=You opened already a support ticket.\n{0} portfolio.pending.disputeOpenedMyUser=You opened already a dispute.\n{0} portfolio.pending.disputeOpenedByPeer=Your trading peer opened a dispute\n{0} portfolio.pending.supportTicketOpenedByPeer=Your trading peer opened a support ticket.\n{0} portfolio.pending.noReceiverAddressDefined=No receiver address defined -portfolio.pending.removeFailedTrade=If the arbitrator could not close that trade you can move it yourself to the failed trades screen.\n\ +portfolio.pending.removeFailedTrade=If the mediator or arbitrator could not close that trade you can move it yourself \ + to the failed trades screen.\n\ Do you want to remove that failed trade from the Pending trades screen? + +portfolio.pending.mediationResult.headline=Suggested payout from mediation +portfolio.pending.mediationResult.info.noneAccepted=Complete the trade by accepting the mediator's suggestion for the trade payout. +portfolio.pending.mediationResult.info.selfAccepted=You have accepted the mediator's suggestion. Waiting for peer to accept as well. +portfolio.pending.mediationResult.info.peerAccepted=Your trade peer has accepted the mediator's suggestion. Do you accept as well? +portfolio.pending.mediationResult.button=Accept or reject +portfolio.pending.mediationResult.popup.headline=Mediation result for trade with ID: {0} +portfolio.pending.mediationResult.popup.headline.peerAccepted=Your trade peer has accepted the mediator''s suggestion for trade {0} +portfolio.pending.mediationResult.popup.info=The mediator has suggested the following payout:\n\ + You receive: {0}\n\ + Your trade peer receives: {1}\n\n\ + You can accept or reject this suggested payout.\n\n\ + By accepting it, you sign the proposed payout transaction. \ + If your trade peer also accepts and signs, the payout will be completed, and the trade is closed.\n\n\ + If one or both parties reject the suggestion, a dispute with an arbitrator will be opened. \ + The arbitrator will investigate the case again and do a payout based on their findings.\n\n\ + Please note that arbitrators are not always online and may take longer to respond than mediators. \ + It can take up to 5 business days for them to respond to messages. +portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration + portfolio.closed.completed=Completed -portfolio.closed.ticketClosed=Ticket closed +portfolio.closed.ticketClosed=Arbitrated +portfolio.closed.mediationTicketClosed=Mediated portfolio.closed.canceled=Canceled portfolio.failed.Failed=Failed @@ -841,9 +897,9 @@ funds.tx.dustAttackTx.popup=This transaction is sending a very small BTC amount # Support #################################################################### -support.tab.support=Support tickets -support.tab.ArbitratorsSupportTickets=Arbitrator's support tickets -support.tab.TradersSupportTickets=Trader's support tickets +support.tab.mediation.support=Mediation +support.tab.arbitration.support=Arbitration +support.tab.ArbitratorsSupportTickets={0}'s tickets support.filter=Filter list support.filter.prompt=Enter trade ID, date, onion address or account data support.noTickets=There are no open tickets @@ -879,20 +935,14 @@ support.buyerOfferer=BTC buyer/Maker support.sellerOfferer=BTC seller/Maker support.buyerTaker=BTC buyer/Taker support.sellerTaker=BTC seller/Taker -support.backgroundInfo=Bisq is not a company, so it handles disputes differently.\n\n\ -If there are disputes in the trade process (e.g. one trader does not follow the trade protocol) \ -the application will display an \"Open dispute\" button after the trade period is over \ -for contacting the arbitrator.\n\n\ -If there is an issue with the application, the software will try to detect it and, if possible, display \ -an \"Open support ticket\" button to contact the arbitrator who will forward the issue \ -to the developers.\n\n\ -If you are having an issue and did not see the \"Open support ticket\" button, \ -you can open a support ticket manually by selecting the trade causing issues \ -under \"Portfolio/Open trades\" and hitting \"alt + o\" or \"option + o\". \ -Please use this method only if you are sure that the software is not working as expected. \ -If you have problems or questions, please review the FAQ on the \ -bisq.network web page or post in the Bisq forum in the Support section. +# TODO @m52go could you provide a good text here? +support.backgroundInfo=Bisq is not a company, so it handles disputes differently.\n\n\ +Traders can communicate within the application via a secure chat on the pending trades screen to attempt solving a dispute on their own. \ + If that is not sufficient, a mediator can step in to help. The mediator will evaluate the situation and give a recommendation for the \ + payout of the trade funds. If both traders accept this suggestion, the payout transaction is completed and the trade is closed. \ + If one or both traders do not agree to the mediator's recommended payout, they can request arbitration.\ + The arbitrator has the third key of the deposit transaction and will make the payout based on their findings. support.initialInfo=Please enter a description of your problem in the text field below. \ Add as much information as possible to speed up dispute resolution time.\n\n\ Here is a check list for information you should provide:\n\ @@ -906,11 +956,12 @@ support.initialInfo=Please enter a description of your problem in the text field \t Sometimes the data directory gets corrupted and leads to strange bugs. \n\ \t See: https://docs.bisq.network/backup-recovery.html#switch-to-a-new-data-directory\n\n\ Please make yourself familiar with the basic rules for the dispute process:\n\ -\t● You need to respond to the arbitrator's requests within 2 days.\n\ +\t● You need to respond to the {0}}''s requests within 2 days.\n\ +\t● Mediators respond in between 2 days. Arbitrators respond in between 5 business days.\n\ \t● The maximum period for a dispute is 14 days.\n\ -\t● You need to cooperate with the arbitrator and provide the information they request to make your case.\n\ +\t● You need to cooperate with the {1} and provide the information they request to make your case.\n\ \t● You accepted the rules outlined in the dispute document in the user agreement when you first started the application.\n\n\ -You can read more about the dispute process at: https://bisq.network/docs/exchange/arbitration-system +You can read more about the dispute process at: {2} support.systemMsg=System message: {0} support.youOpenedTicket=You opened a request for support.\n\n{0}\n\nBisq version: {1} support.youOpenedDispute=You opened a request for a dispute.\n\n{0}\n\nBisq version: {1} @@ -1046,13 +1097,18 @@ setting.about.subsystems.val=Network version: {0}; P2P message version: {1}; Loc #################################################################### account.tab.arbitratorRegistration=Arbitrator registration +account.tab.mediatorRegistration=Mediator registration account.tab.account=Account account.info.headline=Welcome to your Bisq Account -account.info.msg=Here you can add trading accounts for national currencies & altcoins, select arbitrators, and create a backup of your wallet & account data.\n\n\ +account.info.msg=Here you can add trading accounts for national currencies & altcoins and create a backup of your wallet & account data.\n\n\ A new Bitcoin wallet was created the first time you started Bisq.\n\n\ -We strongly recommend that you write down your Bitcoin wallet seed words (see tab on the top) and consider adding a password before funding. Bitcoin deposits and withdrawals are managed in the \"Funds\" section.\n\n\ +We strongly recommend that you write down your Bitcoin wallet seed words (see tab on the top) and consider adding a \ + password before funding. Bitcoin deposits and withdrawals are managed in the \"Funds\" section.\n\n\ Privacy & security note: \ -because Bisq is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no access to your personal info, your funds, or even your IP address. Data such as bank account numbers, altcoin & Bitcoin addresses, etc are only shared with your trading partner to fulfill trades you initiate (in case of a dispute the arbitrator will see the same data as your trading peer). +because Bisq is a decentralized exchange, all your data is kept on your computer. There are no servers, so we have no \ + access to your personal info, your funds, or even your IP address. Data such as bank account numbers, \ + altcoin & Bitcoin addresses, etc are only shared with your trading partner to fulfill trades you initiate \ + (in case of a dispute the mediator or arbitrator will see the same data as your trading peer). account.menu.paymentAccount=National currency accounts account.menu.altCoinsAccountView=Altcoin accounts @@ -1061,32 +1117,23 @@ account.menu.seedWords=Wallet seed account.menu.backup=Backup account.menu.notifications=Notifications +## TODO should we rename the following to a gereric name? account.arbitratorRegistration.pubKey=Public key -account.arbitratorRegistration.register=Register arbitrator -account.arbitratorRegistration.revoke=Revoke registration -account.arbitratorRegistration.info.msg=Please note that you need to stay available for 15 days after revoking as there might be trades which are using you as arbitrator. The max. allowed trade period is 8 days and the dispute process might take up to 7 days. +account.arbitratorRegistration.register=Register +account.arbitratorRegistration.registration={0} registration +account.arbitratorRegistration.revoke=Revoke +account.arbitratorRegistration.info.msg=Please note that you need to stay available for 15 days after revoking as there might be trades which are using you as {0}}. The max. allowed trade period is 8 days and the dispute process might take up to 7 days. account.arbitratorRegistration.warn.min1Language=You need to set at least 1 language.\nWe added the default language for you. -account.arbitratorRegistration.removedSuccess=You have successfully removed your arbitrator from the Bisq network. -account.arbitratorRegistration.removedFailed=Could not remove arbitrator.{0} -account.arbitratorRegistration.registerSuccess=You have successfully registered your arbitrator to the Bisq network. -account.arbitratorRegistration.registerFailed=Could not register arbitrator.{0} - -account.arbitratorSelection.minOneArbitratorRequired=You need to set at least 1 language.\nWe added the default language for you. -account.arbitratorSelection.whichLanguages=Which languages do you speak? -account.arbitratorSelection.whichDoYouAccept=Which arbitrators do you accept -account.arbitratorSelection.autoSelect=Auto select all arbitrators with matching language -account.arbitratorSelection.regDate=Registration date -account.arbitratorSelection.languages=Languages -account.arbitratorSelection.cannotSelectHimself=An arbitrator cannot select himself for trading. -account.arbitratorSelection.noMatchingLang=No matching language. -account.arbitratorSelection.noLang=You can only select arbitrators who are speaking at least 1 common language. -account.arbitratorSelection.minOne=You need to have at least one arbitrator selected. +account.arbitratorRegistration.removedSuccess=You have successfully removed your registration from the Bisq network. +account.arbitratorRegistration.removedFailed=Could not remove registration.{0} +account.arbitratorRegistration.registerSuccess=You have successfully registered to the Bisq network. +account.arbitratorRegistration.registerFailed=Could not complete registration.{0} account.altcoin.yourAltcoinAccounts=Your altcoin accounts account.altcoin.popup.wallet.msg=Please be sure that you follow the requirements for the usage of {0} wallets as \ described on the {1} web page.\nUsing wallets from centralized exchanges where (a) you don''t control your keys or \ -(b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe arbitrator is \ +(b) which don''t use compatible wallet software is risky: it can lead to loss of the traded funds!\nThe mediator or arbitrator is \ not a {2} specialist and cannot help in such cases. account.altcoin.popup.wallet.confirm=I understand and confirm that I know which wallet I need to use. account.altcoin.popup.arq.msg=Trading ARQ on Bisq requires that you understand and fulfill \ @@ -1097,13 +1144,13 @@ that would be required in case of a dispute.\n\ arqma-wallet-cli (use the command get_tx_key)\n\ arqma-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\n\ At normal block explorers the transfer is not verifiable.\n\n\ -You need to provide the arbitrator the following data in case of a dispute:\n\ +You need to provide the mediator or arbitrator the following data in case of a dispute:\n\ - The tx private key\n\ - The transaction hash\n\ - The recipient's public address\n\n\ Failure to provide the above data, or if you used an incompatible wallet, will result in losing the \ dispute case. The ARQ sender is responsible for providing verification of the ARQ transfer to the \ -arbitrator in case of a dispute.\n\n\ +mediator or arbitrator in case of a dispute.\n\n\ There is no payment ID required, just the normal public address.\n\ If you are not sure about that process visit ArQmA discord channel (https://discord.gg/s9BQpJT) \ or the ArQmA forum (https://labs.arqma.com) to find more information. @@ -1118,16 +1165,17 @@ In addition to XMR checktx tool (https://xmr.llcoins.net/checktx.html) verificat monero-wallet-cli : using command (check_tx_key).\n\ monero-wallet-gui : on the Advanced > Prove/Check page.\n\ At normal block explorers the transfer is not verifiable.\n\n\ -You need to provide the arbitrator the following data in case of a dispute:\n\ +You need to provide the mediator or arbitrator the following data in case of a dispute:\n\ - The tx private key\n\ - The transaction hash\n\ - The recipient's public address\n\n\ Failure to provide the above data, or if you used an incompatible wallet, will result in losing the \ dispute case. The XMR sender is responsible for providing verification of the XMR transfer to the \ -arbitrator in case of a dispute.\n\n\ +mediator or arbitrator in case of a dispute.\n\n\ There is no payment ID required, just the normal public address.\n\ If you are not sure about that process visit (https://www.getmonero.org/resources/user-guides/prove-payment.html) \ or the Monero forum (https://forum.getmonero.org) to find more information. +# suppress inspection "TrailingSpacesInProperty" account.altcoin.popup.msr.msg=Trading MSR on Bisq requires that you understand and fulfill \ the following requirements:\n\n\ For sending MSR, you need to use either the official Masari GUI wallet, Masari CLI wallet with the \ @@ -1135,20 +1183,20 @@ store-tx-info flag enabled (enabled by default) or the Masari web wallet (https: that would be required in case of a dispute.\n\ masari-wallet-cli (use the command get_tx_key)\n\ masari-wallet-gui (go to history tab and click on the (P) button for payment proof)\n\n\ -Masari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\n +Masari Web Wallet (goto Account -> transaction history and view details on your sent transaction)\n\n\ Verification can be accomplished in-wallet.\n\ masari-wallet-cli : using command (check_tx_key).\n\ masari-wallet-gui : on the Advanced > Prove/Check page.\n\ Verification can be accomplished in the block explorer \n\ Open block explorer (https://explorer.getmasari.org), use the search bar to find your transaction hash.\n\ Once transaction is found, scroll to bottom to the 'Prove Sending' area and fill in details as needed.\n\ -You need to provide the arbitrator the following data in case of a dispute:\n\ +You need to provide the mediator or arbitrator the following data in case of a dispute:\n\ - The tx private key\n\ - The transaction hash\n\ - The recipient's public address\n\n\ Failure to provide the above data, or if you used an incompatible wallet, will result in losing the \ dispute case. The MSR sender is responsible for providing verification of the MSR transfer to the \ -arbitrator in case of a dispute.\n\n\ +mediator or arbitrator in case of a dispute.\n\n\ There is no payment ID required, just the normal public address.\n\ If you are not sure about that process, ask for help on the Official Masari Discord (https://discord.gg/sMCwMqs). account.altcoin.popup.blur.msg=Trading BLUR on Bisq requires that you understand and fulfill \ @@ -1160,11 +1208,11 @@ transaction private key. If you fail to perform this step, you may not be able t If you are using the Blur Network GUI Wallet, the transaction private key and transaction ID can be found conveniently \ in the "History" tab. Immediately after sending, locate the transaction of interest. Click the "?" symbol in the \ lower-right corner of the box containing the transaction. You must save this information. \n\n\ -In the event that arbitration is necessary, you must present the following to an arbitrator: 1.) the transaction ID, \ -2.) the transaction private key, and 3.) the recipient's address. The arbitrator will then verify the BLUR \ +In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, \ +2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the BLUR \ transfer using the Blur Transaction Viewer (https://blur.cash/#tx-viewer).\n\n\ -Failure to provide the required information to the arbitrator will result in losing the dispute case. In all cases of dispute, the \ -BLUR sender bears 100% of the burden of responsibility in verifying transactions to an arbitrator. \n\n\ +Failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ +BLUR sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ If you do not understand these requirements, do not trade on Bisq. First, seek help at the Blur Network Discord (https://discord.gg/dMWaqVW). account.altcoin.popup.solo.msg=Trading Solo on Bisq requires that you understand and fulfill \ the following requirements:\n\n\ @@ -1172,11 +1220,11 @@ To send Solo you must use the Solo Network CLI Wallet. \n\n\ If you are using the CLI wallet, a transaction hash (tx ID) will be displayed after a transfer is sent. You must save \ this information. Immediately after sending the transfer, you must use the command 'get_tx_key' to retrieve the \ transaction private key. If you fail to perform this step, you may not be able to retrieve the key later. \n\n\ -In the event that arbitration is necessary, you must present the following to an arbitrator: 1.) the transaction ID, \ -2.) the transaction private key, and 3.) the recipient's address. The arbitrator will then verify the Solo \ +In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1.) the transaction ID, \ +2.) the transaction private key, and 3.) the recipient's address. The mediator or arbitrator will then verify the Solo \ transfer using the Solo Block Explorer by searching for the transaction and then using the "Prove sending" function (https://explorer.minesolo.com/).\n\n\ -failure to provide the required information to the arbitrator will result in losing the dispute case. In all cases of dispute, the \ -Solo sender bears 100% of the burden of responsibility in verifying transactions to an arbitrator. \n\n\ +failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ +Solo sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ If you do not understand these requirements, do not trade on Bisq. First, seek help at the Solo Network Discord (https://discord.minesolo.com/). account.altcoin.popup.cash2.msg=Trading CASH2 on Bisq requires that you understand and fulfill \ the following requirements:\n\n\ @@ -1184,11 +1232,11 @@ To send CASH2 you must use the Cash2 Wallet version 3 or higher. \n\n\ After a transaction is sent, the transaction ID will be displayed. You must save this information. \ Immediately after sending the transaction, you must use the command 'getTxKey' in simplewallet to retrieve the \ transaction secret key. \n\n\ -In the event that arbitration is necessary, you must present the following to an arbitrator: 1) the transaction ID, \ -2) the transaction secret key, and 3) the recipient's Cash2 address. The arbitrator will then verify the CASH2 \ +In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, \ +2) the transaction secret key, and 3) the recipient's Cash2 address. The mediator or arbitrator will then verify the CASH2 \ transfer using the Cash2 Block Explorer (https://blocks.cash2.org).\n\n\ -Failure to provide the required information to the arbitrator will result in losing the dispute case. In all cases of dispute, the \ -CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an arbitrator. \n\n\ +Failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ +CASH2 sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ If you do not understand these requirements, do not trade on Bisq. First, seek help at the Cash2 Discord (https://discord.gg/FGfXAYN). account.altcoin.popup.qwertycoin.msg=Trading Qwertycoin on Bisq requires that you understand and fulfill \ the following requirements:\n\n\ @@ -1196,11 +1244,11 @@ To send QWC you must use the official QWC Wallet version 5.1.3 or higher. \n\n\ After a transaction is sent, the transaction ID will be displayed. You must save this information. \ Immediately after sending the transaction, you must use the command 'get_Tx_Key' in simplewallet to retrieve the \ transaction secret key. \n\n\ -In the event that arbitration is necessary, you must present the following to an arbitrator: 1) the transaction ID, \ -2) the transaction secret key, and 3) the recipient's QWC address. The arbitrator will then verify the QWC \ +In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the transaction ID, \ +2) the transaction secret key, and 3) the recipient's QWC address. The mediator or arbitrator will then verify the QWC \ transfer using the QWC Block Explorer (https://explorer.qwertycoin.org).\n\n\ -Failure to provide the required information to the arbitrator will result in losing the dispute case. In all cases of dispute, the \ -QWC sender bears 100% of the burden of responsibility in verifying transactions to an arbitrator. \n\n\ +Failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ +QWC sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ If you do not understand these requirements, do not trade on Bisq. First, seek help at the QWC Discord (https://discord.gg/rUkfnpC). account.altcoin.popup.drgl.msg=Trading Dragonglass on Bisq requires that you understand and fulfill \ the following requirements:\n\n\ @@ -1210,19 +1258,19 @@ The TXN-Private Key is a one-time key automatically generated for every transact only be accessed from within your DRGL wallet.\n\ Either by DRGL-wallet GUI (inside transaction details dialog) or by the Dragonglass CLI simplewallet (using command "get_tx_key").\n\n\ DRGL version 'Oathkeeper' and higher are REQUIRED for both.\n\n\ -In case of a dispute, you must provide the arbitrator the following data:\n\ +In case of a dispute, you must provide the mediator or arbitrator the following data:\n\ - The TXN-Private key\n\ - The transaction hash\n\ - The recipient's public address\n\n\ Verification of payment can be made using the above data as inputs at (http://drgl.info/#check_txn).\n\n\ Failure to provide the above data, or if you used an incompatible wallet, will result in losing the \ dispute case. The Dragonglass sender is responsible for providing verification of the DRGL transfer to the \ -arbitrator in case of a dispute. Use of PaymentID is not required.\n\n\ +mediator or arbitrator in case of a dispute. Use of PaymentID is not required.\n\n\ If you are unsure about any part of this process, visit Dragonglass on Discord (http://discord.drgl.info) for help. account.altcoin.popup.ZEC.msg=When using Zcash you can only use the transparent addresses (starting with t), not \ -the z-addresses (private), because the arbitrator would not be able to verify the transaction with z-addresses. +the z-addresses (private), because the mediator or arbitrator would not be able to verify the transaction with z-addresses. account.altcoin.popup.XZC.msg=When using Zcoin you can only use the transparent (traceable) addresses, not \ -the untraceable addresses, because the arbitrator would not be able to verify the transaction with untraceable addresses at a block explorer. +the untraceable addresses, because the mediator or arbitrator would not be able to verify the transaction with untraceable addresses at a block explorer. account.altcoin.popup.grin.msg=GRIN requires an interactive process between the sender and receiver to create the \ transaction. Be sure to follow the instructions from the GRIN project web page to reliably send and receive GRIN \ (the receiver needs to be online or at least be online during a certain time frame). \n\n\ @@ -1245,11 +1293,11 @@ the following requirements:\n\n\ To send PARS you must use the official ParsiCoin Wallet version 3.0.0 or higher. \n\n\ You can Check your Transaction Hash and Transaction Key on Transactions Section on your GUI Wallet (ParsiPay) \ You need to right Click on the Transaction and then click on show details. \n\n\ -In the event that arbitration is necessary, you must present the following to an arbitrator: 1) the Transaction Hash, \ -2) the Transaction Key, and 3) the recipient's PARS address. The arbitrator will then verify the PARS \ +In the event that arbitration is necessary, you must present the following to an mediator or arbitrator: 1) the Transaction Hash, \ +2) the Transaction Key, and 3) the recipient's PARS address. The mediator or arbitrator will then verify the PARS \ transfer using the ParsiCoin Block Explorer (http://explorer.parsicoin.net/#check_payment).\n\n\ -Failure to provide the required information to the arbitrator will result in losing the dispute case. In all cases of dispute, the \ -ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an arbitrator. \n\n\ +Failure to provide the required information to the mediator or arbitrator will result in losing the dispute case. In all cases of dispute, the \ +ParsiCoin sender bears 100% of the burden of responsibility in verifying transactions to an mediator or arbitrator. \n\n\ If you do not understand these requirements, do not trade on Bisq. First, seek help at the ParsiCoin Discord (https://discord.gg/c7qmFNh). account.altcoin.popup.blk-burnt.msg=To trade burnt blackcoins, you need to know the following:\n\n\ @@ -2124,10 +2172,6 @@ displayUpdateDownloadWindow.download.openDir=Open download directory disputeSummaryWindow.title=Summary disputeSummaryWindow.openDate=Ticket opening date disputeSummaryWindow.role=Trader's role -disputeSummaryWindow.evidence=Evidence -disputeSummaryWindow.evidence.tamperProof=Tamper proof evidence -disputeSummaryWindow.evidence.id=ID Verification -disputeSummaryWindow.evidence.video=Video/Screencast disputeSummaryWindow.payout=Trade amount payout disputeSummaryWindow.payout.getsTradeAmount=BTC {0} gets trade amount payout disputeSummaryWindow.payout.getsAll=BTC {0} gets all @@ -2150,12 +2194,9 @@ disputeSummaryWindow.addSummaryNotes=Add summary notes disputeSummaryWindow.close.button=Close ticket disputeSummaryWindow.close.msg=Ticket closed on {0}\n\n\ Summary:\n\ -{1} delivered tamper proof evidence: {2}\n\ -{3} did ID verification: {4}\n\ -{5} did screencast or video: {6}\n\ -Payout amount for BTC buyer: {7}\n\ -Payout amount for BTC seller: {8}\n\n\ -Summary notes:\n{9} +Payout amount for BTC buyer: {1}\n\ +Payout amount for BTC seller: {2}\n\n\ +Summary notes:\n{3} disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket! emptyWalletWindow.headline={0} emergency wallet tool @@ -2173,7 +2214,7 @@ emptyWalletWindow.openOffers.warn=You have open offers which will be removed if emptyWalletWindow.openOffers.yes=Yes, I am sure emptyWalletWindow.sent.success=The balance of your wallet was successfully transferred. -enterPrivKeyWindow.headline=Registration open for invited arbitrators only +enterPrivKeyWindow.headline=Enter private key for registration filterWindow.headline=Edit filter list filterWindow.offers=Filtered offers (comma sep.) @@ -2182,6 +2223,7 @@ filterWindow.accounts=Filtered trading account data:\nFormat: comma sep. list of filterWindow.bannedCurrencies=Filtered currency codes (comma sep.) filterWindow.bannedPaymentMethods=Filtered payment method IDs (comma sep.) filterWindow.arbitrators=Filtered arbitrators (comma sep. onion addresses) +filterWindow.mediators=Filtered mediators (comma sep. onion addresses) filterWindow.seedNode=Filtered seed nodes (comma sep. onion addresses) filterWindow.priceRelayNode=Filtered price relay nodes (comma sep. onion addresses) filterWindow.btcNode=Filtered Bitcoin nodes (comma sep. addresses + port) @@ -2200,7 +2242,6 @@ offerDetailsWindow.offererBankId=(maker's bank ID/BIC/SWIFT) offerDetailsWindow.offerersBankName=(maker's bank name) offerDetailsWindow.bankId=Bank ID (e.g. BIC or SWIFT) offerDetailsWindow.countryBank=Maker's country of bank -offerDetailsWindow.acceptedArbitrators=Accepted arbitrators offerDetailsWindow.commitment=Commitment offerDetailsWindow.agree=I agree offerDetailsWindow.tac=Terms and conditions @@ -2260,6 +2301,7 @@ tradeDetailsWindow.tradeDate=Trade date tradeDetailsWindow.txFee=Mining fee tradeDetailsWindow.tradingPeersOnion=Trading peers onion address tradeDetailsWindow.tradeState=Trade state +tradeDetailsWindow.agentAddresses=Arbitrator/Mediator walletPasswordWindow.headline=Enter password to unlock @@ -2337,10 +2379,13 @@ Please restart the application. popup.warning.startupFailed.twoInstances=Bisq is already running. You cannot run two instances of Bisq. popup.warning.cryptoTestFailed=Seems that you use a self compiled binary and have not following the build instructions in https://github.com/bisq-network/exchange/blob/master/doc/build.md#7-enable-unlimited-strength-for-cryptographic-keys.\n\nIf that is not the case and you use the official Bisq binary, please file a bug report to the GitHub page.\nError={0} popup.warning.tradePeriod.halfReached=Your trade with ID {0} has reached the half of the max. allowed trading period and is still not completed.\n\nThe trade period ends on {1}\n\nPlease check your trade state at \"Portfolio/Open trades\" for further information. -popup.warning.tradePeriod.ended=Your trade with ID {0} has reached the max. allowed trading period and is not completed.\n\nThe trade period ended on {1}\n\nPlease check your trade at \"Portfolio/Open trades\" for contacting the arbitrator. +popup.warning.tradePeriod.ended=Your trade with ID {0} has reached the max. allowed trading period and is not completed.\n\n\ + The trade period ended on {1}\n\n\ + Please check your trade at \"Portfolio/Open trades\" for contacting the mediator. popup.warning.noTradingAccountSetup.headline=You have not setup a trading account popup.warning.noTradingAccountSetup.msg=You need to setup a national currency or altcoin account before you can create an offer.\nDo you want to setup an account? popup.warning.noArbitratorsAvailable=There are no arbitrators available. +popup.warning.noMediatorsAvailable=There are no mediators available. popup.warning.notFullyConnected=You need to wait until you are fully connected to the network.\nThat might take up to about 2 minutes at startup. popup.warning.notSufficientConnectionsToBtcNetwork=You need to wait until you have at least {0} connections to the Bitcoin network. popup.warning.downloadNotComplete=You need to wait until the download of missing Bitcoin blocks is complete. @@ -2427,6 +2472,7 @@ popup.dao.launch.governance=Bisq’s trading network was already decentralized. popup.dao.launch.trading.title=Trade popup.dao.launch.trading=Trade BSQ (colored bitcoin) to participate in Bisq governance. You can buy and sell BSQ just like any other asset on Bisq. popup.dao.launch.cheaperFees.title=Cheaper fees +# suppress inspection "TrailingSpacesInProperty" popup.dao.launch.cheaperFees=Get a 90% discount on trading fees when you use BSQ. Save money and support the project at the same time!\n\n #################################################################### @@ -2778,7 +2824,7 @@ payment.revolut.info=Please be sure that the phone number you used for your Revo payment.usPostalMoneyOrder.info=Money orders are one of the more private fiat purchase methods available on Bisq.\n\n\ However, please be aware of potentially increased risks associated with their use. Bisq will not bear any \ - responsibility in case a sent money order is stolen, and the arbitrators will in such cases award the BTC \ + responsibility in case a sent money order is stolen, and the mediator or arbitrator will in such cases award the BTC \ to the sender of the money order, provided they can produce tracking information and receipts. \ It may be advisable for the sender to write the BTC seller's name on the money order, in order to minimize the \ risk that the money order is cashed by someone else. @@ -2797,7 +2843,7 @@ payment.f2f.info='Face to Face' trades have different rules and come with differ ● The trading peers need to bring their laptops and do the confirmation of 'payment sent' and 'payment received' at the meeting place.\n\ ● If a maker has special 'terms and conditions' they must state those in the 'Additional information' text field in the account.\n\ ● By taking an offer the taker agrees to the maker's stated 'terms and conditions'.\n\ - ● In case of a dispute the arbitrator cannot help much as it is usually hard to get tamper proof evidence of what \ + ● In case of a dispute the mediator or arbitrator cannot help much as it is usually hard to get tamper proof evidence of what \ happened at the meeting. In such cases the BTC funds might get locked indefinitely or until the trading peers come to \ an agreement.\n\n\ To be sure you fully understand the differences with 'Face to Face' trades please read the instructions and \ diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index 961009dc6e..b7117fad48 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -669,7 +669,7 @@ portfolio.pending.openSupportTicket.headline=Support-Ticket öffnen portfolio.pending.openSupportTicket.msg=Nutzen Sie dies bitte nur in Notfällen, wenn Ihnen die \"Support öffnen\"- oder \"Konflikt öffnen\"-Schaltflächen nicht angezeigt wird.\n\nSollten Sie ein Support-Ticket öffnen, wird der Handel unterbrochen und vom Vermittler bearbeitet. portfolio.pending.notification=Benachrichtigung portfolio.pending.openDispute=Einen Konflikt öffnen -portfolio.pending.disputeOpened=Konflikt geöffnet +portfolio.pending.arbitrationRequested=Konflikt geöffnet portfolio.pending.openSupport=Support-Ticket öffnen portfolio.pending.supportTicketOpened=Support-Ticket geöffnet portfolio.pending.requestSupport=Support anfordern @@ -765,7 +765,7 @@ funds.tx.dustAttackTx.popup=Diese Transaktion sendet einen sehr kleinen BTC Betr # Support #################################################################### -support.tab.support=Support-Tickets +support.tab.mediation.support=Support-Tickets support.tab.ArbitratorsSupportTickets=Support-Tickets des Vermittlers support.tab.TradersSupportTickets=Support-Tickets des Händlers support.filter=Liste filtern diff --git a/core/src/main/resources/i18n/displayStrings_el.properties b/core/src/main/resources/i18n/displayStrings_el.properties index 8c52eb0dfc..0441cce7b3 100644 --- a/core/src/main/resources/i18n/displayStrings_el.properties +++ b/core/src/main/resources/i18n/displayStrings_el.properties @@ -669,7 +669,7 @@ portfolio.pending.openSupportTicket.headline=Άνοιξε αίτημα υποσ portfolio.pending.openSupportTicket.msg=Χρησιμοποίησέ το μονάχα σε επείγουσες περιπτώσεις, αν δεν εμαφανίζεται το κουμπί \"Αίτημα υποστήριξης\" ή \"Επίλυση διένεξης\".\n\nΑνοίγοντας ένα αίτημα υποστήριξης η συναλλαγή αναστέλλεται και τη διαχείριση αναλαμβάνει ο διαμεσολαβητής portfolio.pending.notification=Ειδοποίηση portfolio.pending.openDispute=Άνοιξε την επίλυση διένεξης -portfolio.pending.disputeOpened=Η επίλυση διένεξης άνοιξε +portfolio.pending.arbitrationRequested=Η επίλυση διένεξης άνοιξε portfolio.pending.openSupport=Άνοιξε αίτημα υποστήριξης portfolio.pending.supportTicketOpened=Το αίτημα υποστήριξης άνοιξε portfolio.pending.requestSupport=Αίτηση υποστήριξης @@ -765,7 +765,7 @@ funds.tx.dustAttackTx.popup=Αυτή η συναλλαγή στέλνει ένα # Support #################################################################### -support.tab.support=Αιτήματα υποστήριξης +support.tab.mediation.support=Αιτήματα υποστήριξης support.tab.ArbitratorsSupportTickets=Αιτήματα υποστήριξης διαμεσολαβητή support.tab.TradersSupportTickets=Αιτήματα υποστήριξης συναλλασσόμενου support.filter=Λίστα φίλτρων diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index c063f976d9..c7b9c1276e 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -669,7 +669,7 @@ portfolio.pending.openSupportTicket.headline=Abrir ticket de soporte portfolio.pending.openSupportTicket.msg=Por favor usar sólo en caso de emergencia si no se muestra el botón \"Abrir soporte\" o \"Abrir disputa\".\n\nCuando abra un ticket de soporte el intercambio se interrumpirá y será manejado por el árbitro. portfolio.pending.notification=Notificación portfolio.pending.openDispute=Abrir una disputa -portfolio.pending.disputeOpened=Disputa abierta +portfolio.pending.arbitrationRequested=Disputa abierta portfolio.pending.openSupport=Abrir ticket de soporte portfolio.pending.supportTicketOpened=Ticket de soporte abierto portfolio.pending.requestSupport=Solicitar soporte @@ -765,7 +765,7 @@ funds.tx.dustAttackTx.popup=Esta transacción está enviando una cantidad de BTC # Support #################################################################### -support.tab.support=Tickets de soporte +support.tab.mediation.support=Tickets de soporte support.tab.ArbitratorsSupportTickets=Tickets de soporte del árbitro support.tab.TradersSupportTickets=Tickets de soporte de comerciante support.filter=Filtrar lista diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 107021b029..304d2ee4db 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -669,7 +669,7 @@ portfolio.pending.openSupportTicket.headline=باز کردن تیکت پشتیب portfolio.pending.openSupportTicket.msg= لطفاً تنها در موارد اضطراری که دکمه‌ی \"باز کردن پشتیبانی\" یا \"باز کردن مناقشه\" برای شما به نمایش درنیامده است، از آن استفاده کنید.\n\nوقتی شما یک تیکت پشتیبانی را باز می‌کنید، معامله متوقف شده و توسط داور رسیدگی خواهد شد. portfolio.pending.notification=اطلاع رسانی portfolio.pending.openDispute=باز کردن مناقشه -portfolio.pending.disputeOpened=مناقشه باز شده است +portfolio.pending.arbitrationRequested=مناقشه باز شده است portfolio.pending.openSupport=باز کردن تیکت پشتیبانی portfolio.pending.supportTicketOpened=تیکت پشتیبانی باز شد portfolio.pending.requestSupport=درخواست پشتیبانی @@ -765,7 +765,7 @@ funds.tx.dustAttackTx.popup=This transaction is sending a very small BTC amount # Support #################################################################### -support.tab.support=تیکت‌های پشتیبانی +support.tab.mediation.support=تیکت‌های پشتیبانی support.tab.ArbitratorsSupportTickets=تیکت‌های پشتیبانی داور support.tab.TradersSupportTickets=تیکت‌های پشتیبانی معامله گر support.filter=لیست فیلتر diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index 196932fff5..12ade104e6 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -669,7 +669,7 @@ portfolio.pending.openSupportTicket.headline=Ouvrir un ticket d'assistance portfolio.pending.openSupportTicket.msg=S'il vous plaît utilisez ceci seulement en cas d'urgence si vous ne pouvez pas afficher le bouton \"Open support\" or \"Ouvrir un litige\.\n\nLorsque vous ouvrez un ticket de support, la transaction est interrompue et traitée par l'arbitre. portfolio.pending.notification=Notification portfolio.pending.openDispute=Déclencher un litige -portfolio.pending.disputeOpened=Litige ouvert +portfolio.pending.arbitrationRequested=Litige ouvert portfolio.pending.openSupport=Ouvrir un ticket d'assistance portfolio.pending.supportTicketOpened=Ticket d'assistance ouvert portfolio.pending.requestSupport=Demander de l'aide @@ -765,7 +765,7 @@ funds.tx.dustAttackTx.popup=Cette transaction va envoyer un faible montant en BT # Support #################################################################### -support.tab.support=Tickets d'assistance +support.tab.mediation.support=Tickets d'assistance support.tab.ArbitratorsSupportTickets=Tickets de support de l'arbitre support.tab.TradersSupportTickets=Tickets de support du trader support.filter=Liste de filtre diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index ea7069a339..1e1509dc64 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -669,7 +669,7 @@ portfolio.pending.openSupportTicket.headline=サポートチケットをオー portfolio.pending.openSupportTicket.msg=あなたが「サポートオープン」または「係争オープン」ボタンが表示されない場合、これは緊急時にのみ使用してください。\n\nサポートチケットを開くと、取引は中断され、調停人が処理します portfolio.pending.notification=通知 portfolio.pending.openDispute=係争を開始 -portfolio.pending.disputeOpened=オープンされた係争 +portfolio.pending.arbitrationRequested=オープンされた係争 portfolio.pending.openSupport=サポートチケットをオープン portfolio.pending.supportTicketOpened=サポートチケットがオープンされた portfolio.pending.requestSupport=サポートをリクエスト @@ -765,7 +765,7 @@ funds.tx.dustAttackTx.popup=このトランザクションはごくわずかなB # Support #################################################################### -support.tab.support=サポートチケット +support.tab.mediation.support=サポートチケット support.tab.ArbitratorsSupportTickets=調停人のサポートチケット support.tab.TradersSupportTickets=取引者のサポートチケット support.filter=フィルターリスト diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 85ac1b01ac..11c503aaeb 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -669,7 +669,7 @@ portfolio.pending.openSupportTicket.headline=Abrir bilhete de apoio portfolio.pending.openSupportTicket.msg=Por favor use isso apenas em caso de emergência, se você não for exibido um botão de \"Abrir apoio\" ou \"Abrir disputa\".\n\nQuando você abre um bilhete de apoio, o negócio será interrompido e tratado pelo árbitro portfolio.pending.notification=Notificação portfolio.pending.openDispute=Abrir uma disputa -portfolio.pending.disputeOpened=Disputa aberta +portfolio.pending.arbitrationRequested=Disputa aberta portfolio.pending.openSupport=Abrir bilhete de apoio portfolio.pending.supportTicketOpened=Bilhete de apoio aberto portfolio.pending.requestSupport=Solicitar apoio @@ -765,7 +765,7 @@ funds.tx.dustAttackTx.popup=Esta transação está enviando uma quantia muito pe # Support #################################################################### -support.tab.support=Bilhetes de apoio +support.tab.mediation.support=Bilhetes de apoio support.tab.ArbitratorsSupportTickets=Bilhetes de apoio do árbitro support.tab.TradersSupportTickets=Bilhetes de apoio do negociador support.filter=Lista de filtros diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index c59be18ac1..fd59a9f03c 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -669,7 +669,7 @@ portfolio.pending.openSupportTicket.headline=Обратиться за подд portfolio.pending.openSupportTicket.msg=Используйте эту функцию только при крайней необходимости, если у вас не появилась кнопка \«Обратиться за поддержкой\» или \«Начать спор\».\n\nПри обращении за поддержкой сделка будет прервана и проведена арбитром. portfolio.pending.notification=Уведомление portfolio.pending.openDispute=Начать спор -portfolio.pending.disputeOpened=Спор начат +portfolio.pending.arbitrationRequested=Спор начат portfolio.pending.openSupport=Обратиться за поддержкой portfolio.pending.supportTicketOpened=Запрос на поддержку отправлен portfolio.pending.requestSupport=Запрос поддержки @@ -765,7 +765,7 @@ funds.tx.dustAttackTx.popup=Вы получили очень маленькую # Support #################################################################### -support.tab.support=Обращения в поддержку +support.tab.mediation.support=Обращения в поддержку support.tab.ArbitratorsSupportTickets=Обращения в поддержку арбитра support.tab.TradersSupportTickets=Обращения в поддержку трейдера support.filter=Фильтры diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index 17c7a1d0a3..3deda3420e 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -669,7 +669,7 @@ portfolio.pending.openSupportTicket.headline=เปิดปุ่มช่ว portfolio.pending.openSupportTicket.msg=โปรดใช้ในกรณีฉุกเฉินเท่านั้น หากปุ่ม \"เปิดการช่วยเหลือและสนับสนุน \" หรือ \"เปิดข้อพิพาท \" ไม่ปรากฏขึ้น\n\nเมื่อคุณเปิดการช่วยเหลือ การซื้อขายจะถูกขัดจังหวะและดำเนินการโดยผู้ไกล่เกลี่ย portfolio.pending.notification=การแจ้งเตือน portfolio.pending.openDispute=เปิดข้อพิพาท -portfolio.pending.disputeOpened=การพิพาทเปิดแล้ว +portfolio.pending.arbitrationRequested=การพิพาทเปิดแล้ว portfolio.pending.openSupport=เปิดปุ่มช่วยเหลือ portfolio.pending.supportTicketOpened=ปุ่มช่วยเหลือถูกเปิดแล้ว portfolio.pending.requestSupport=ขอการสนับสนุนและช่วยเหลือ @@ -765,7 +765,7 @@ funds.tx.dustAttackTx.popup=This transaction is sending a very small BTC amount # Support #################################################################### -support.tab.support=ศูนย์ช่วยเหลือ +support.tab.mediation.support=ศูนย์ช่วยเหลือ support.tab.ArbitratorsSupportTickets=การช่วยเหลือและสนุบสนุนของผู้ไกล่เกลี่ย support.tab.TradersSupportTickets=ศูนย์ช่วยเหลือและสนับสนุนของผู้ซื้อขาย support.filter=รายการตัวกรอง diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index 704547ac46..e5e159553f 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -669,7 +669,7 @@ portfolio.pending.openSupportTicket.headline=Mở vé hỗ trợ portfolio.pending.openSupportTicket.msg=Vui lòng chỉ sử dụng trong trường hợp khẩn cấp nếu bạn không hiển thị Node \"Mở hỗ trợ\" hoặc \"Mở khiếu nại\".\n\nKhi bạn mở Đơn hỗ trợ, giao dịch sẽ bị gián đoạn và xử lý bởi trọng tài portfolio.pending.notification=Thông báo portfolio.pending.openDispute=Mở khiếu nại -portfolio.pending.disputeOpened=Khiếu nại đã mở +portfolio.pending.arbitrationRequested=Khiếu nại đã mở portfolio.pending.openSupport=Mở đơn hỗ trợ portfolio.pending.supportTicketOpened=Đơn hỗ trợ đã mở portfolio.pending.requestSupport=Yêu cầu hỗ trợ @@ -765,7 +765,7 @@ funds.tx.dustAttackTx.popup=Giao dịch này đang gửi một lượng BTC rấ # Support #################################################################### -support.tab.support=Đơn hỗ trợ +support.tab.mediation.support=Đơn hỗ trợ support.tab.ArbitratorsSupportTickets=Đơn hỗ trợ của trọng tài support.tab.TradersSupportTickets=Đơn hỗ trợ của Thương gia support.filter=Danh sách lọc diff --git a/core/src/main/resources/i18n/displayStrings_zh.properties b/core/src/main/resources/i18n/displayStrings_zh.properties index 3329d8c545..be48c3c54c 100644 --- a/core/src/main/resources/i18n/displayStrings_zh.properties +++ b/core/src/main/resources/i18n/displayStrings_zh.properties @@ -669,7 +669,7 @@ portfolio.pending.openSupportTicket.headline=创建帮助话题 portfolio.pending.openSupportTicket.msg=请在紧急情况下使用,如果您没有显示“创建帮助话题”或“创建纠纷”按钮。\n\n当您打开帮助话题时,交易将被仲裁员中断和处理 portfolio.pending.notification=通知 portfolio.pending.openDispute=创建一个纠纷 -portfolio.pending.disputeOpened=纠纷已创建 +portfolio.pending.arbitrationRequested=纠纷已创建 portfolio.pending.openSupport=创建帮助话题 portfolio.pending.supportTicketOpened=帮助话题已经创建 portfolio.pending.requestSupport=请求帮助 @@ -765,7 +765,7 @@ funds.tx.dustAttackTx.popup=这笔交易是发送一个非常小的比特币金 # Support #################################################################### -support.tab.support=帮助工单 +support.tab.mediation.support=帮助工单 support.tab.ArbitratorsSupportTickets=仲裁员的帮助工单 support.tab.TradersSupportTickets=交易者的帮助工单 support.filter=过滤列表 diff --git a/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java b/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java index 7ac3340a99..941a8996c3 100644 --- a/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java +++ b/core/src/test/java/bisq/core/account/sign/SignedWitnessServiceTest.java @@ -19,8 +19,8 @@ package bisq.core.account.sign; import bisq.core.account.witness.AccountAgeWitness; -import bisq.core.arbitration.ArbitratorManager; -import bisq.core.arbitration.DisputeManager; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.network.p2p.storage.persistence.AppendOnlyDataStoreService; @@ -75,9 +75,9 @@ public class SignedWitnessServiceTest { public void setup() throws Exception { AppendOnlyDataStoreService appendOnlyDataStoreService = mock(AppendOnlyDataStoreService.class); ArbitratorManager arbitratorManager = mock(ArbitratorManager.class); - DisputeManager disputeManager = mock(DisputeManager.class); + ArbitrationManager arbitrationManager = mock(ArbitrationManager.class); when(arbitratorManager.isPublicKeyInList(any())).thenReturn(true); - signedWitnessService = new SignedWitnessService(null, null, null, arbitratorManager, null, appendOnlyDataStoreService, disputeManager, null); + signedWitnessService = new SignedWitnessService(null, null, null, arbitratorManager, null, appendOnlyDataStoreService, arbitrationManager, null); account1DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{1}); account2DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{2}); account3DataHash = org.bitcoinj.core.Utils.sha256hash160(new byte[]{3}); diff --git a/core/src/test/java/bisq/core/arbitration/ArbitratorManagerTest.java b/core/src/test/java/bisq/core/arbitration/ArbitratorManagerTest.java index 6f10abc21b..9a626efac9 100644 --- a/core/src/test/java/bisq/core/arbitration/ArbitratorManagerTest.java +++ b/core/src/test/java/bisq/core/arbitration/ArbitratorManagerTest.java @@ -17,6 +17,9 @@ package bisq.core.arbitration; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorService; import bisq.core.user.User; import bisq.network.p2p.NodeAddress; @@ -41,7 +44,7 @@ public class ArbitratorManagerTest { User user = mock(User.class); ArbitratorService arbitratorService = mock(ArbitratorService.class); - ArbitratorManager manager = new ArbitratorManager(null, arbitratorService, user, null, null, false); + ArbitratorManager manager = new ArbitratorManager(null, arbitratorService, user, null, false); ArrayList languagesOne = new ArrayList() {{ add("en"); @@ -61,15 +64,15 @@ public class ArbitratorManagerTest { languagesTwo, 0L, null, "", null, null, null); - manager.addArbitrator(one, () -> { + manager.addDisputeAgent(one, () -> { }, errorMessage -> { }); - manager.addArbitrator(two, () -> { + manager.addDisputeAgent(two, () -> { }, errorMessage -> { }); - assertTrue(manager.isArbitratorAvailableForLanguage("en")); - assertFalse(manager.isArbitratorAvailableForLanguage("th")); + assertTrue(manager.isAgentAvailableForLanguage("en")); + assertFalse(manager.isAgentAvailableForLanguage("th")); } @Test @@ -77,7 +80,7 @@ public class ArbitratorManagerTest { User user = mock(User.class); ArbitratorService arbitratorService = mock(ArbitratorService.class); - ArbitratorManager manager = new ArbitratorManager(null, arbitratorService, user, null, null, false); + ArbitratorManager manager = new ArbitratorManager(null, arbitratorService, user, null, false); ArrayList languagesOne = new ArrayList() {{ add("en"); @@ -101,15 +104,15 @@ public class ArbitratorManagerTest { add(two.getNodeAddress()); }}; - manager.addArbitrator(one, () -> { + manager.addDisputeAgent(one, () -> { }, errorMessage -> { }); - manager.addArbitrator(two, () -> { + manager.addDisputeAgent(two, () -> { }, errorMessage -> { }); - assertThat(manager.getArbitratorLanguages(nodeAddresses), containsInAnyOrder("en", "es")); - assertThat(manager.getArbitratorLanguages(nodeAddresses), not(containsInAnyOrder("de"))); + assertThat(manager.getDisputeAgentLanguages(nodeAddresses), containsInAnyOrder("en", "es")); + assertThat(manager.getDisputeAgentLanguages(nodeAddresses), not(containsInAnyOrder("de"))); } } diff --git a/core/src/test/java/bisq/core/arbitration/ArbitratorTest.java b/core/src/test/java/bisq/core/arbitration/ArbitratorTest.java index 8f5b81839a..f5e58867ff 100644 --- a/core/src/test/java/bisq/core/arbitration/ArbitratorTest.java +++ b/core/src/test/java/bisq/core/arbitration/ArbitratorTest.java @@ -17,6 +17,8 @@ package bisq.core.arbitration; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; + import bisq.network.p2p.NodeAddress; import bisq.common.crypto.PubKeyRing; diff --git a/core/src/test/java/bisq/core/arbitration/BuyerDataItemTest.java b/core/src/test/java/bisq/core/arbitration/BuyerDataItemTest.java index 9c4078eef8..61dc33b834 100644 --- a/core/src/test/java/bisq/core/arbitration/BuyerDataItemTest.java +++ b/core/src/test/java/bisq/core/arbitration/BuyerDataItemTest.java @@ -1,6 +1,7 @@ package bisq.core.arbitration; import bisq.core.account.witness.AccountAgeWitness; +import bisq.core.support.dispute.arbitration.BuyerDataItem; import bisq.core.payment.payload.PaymentAccountPayload; import org.bitcoinj.core.Coin; diff --git a/core/src/test/java/bisq/core/arbitration/MediatorTest.java b/core/src/test/java/bisq/core/arbitration/MediatorTest.java index c710c838fc..b957698bc3 100644 --- a/core/src/test/java/bisq/core/arbitration/MediatorTest.java +++ b/core/src/test/java/bisq/core/arbitration/MediatorTest.java @@ -17,6 +17,8 @@ package bisq.core.arbitration; +import bisq.core.support.dispute.mediation.mediator.Mediator; + import bisq.network.p2p.NodeAddress; import bisq.common.crypto.PubKeyRing; diff --git a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java index ba7187977a..9fcca9a4de 100644 --- a/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java +++ b/core/src/test/java/bisq/core/offer/OpenOfferManagerTest.java @@ -40,7 +40,8 @@ public class OpenOfferManagerTest { final OpenOfferManager manager = new OpenOfferManager(null, null, p2PService, null, null, null, offerBookService, null, null, null, - null, null, new Storage>(null, null, corruptedDatabaseFilesHandler)); + null, null, null, + new Storage>(null, null, corruptedDatabaseFilesHandler)); AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false); @@ -75,7 +76,8 @@ public class OpenOfferManagerTest { final OpenOfferManager manager = new OpenOfferManager(null, null, p2PService, null, null, null, offerBookService, null, null, null, - null, null, new Storage>(null, null, corruptedDatabaseFilesHandler)); + null, null, null, + new Storage>(null, null, corruptedDatabaseFilesHandler)); AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false); @@ -102,7 +104,8 @@ public class OpenOfferManagerTest { final OpenOfferManager manager = new OpenOfferManager(null, null, p2PService, null, null, null, offerBookService, null, null, null, - null, null, new Storage>(null, null, corruptedDatabaseFilesHandler)); + null, null, null, + new Storage>(null, null, corruptedDatabaseFilesHandler)); AtomicBoolean startEditOfferSuccessful = new AtomicBoolean(false); diff --git a/core/src/test/java/bisq/core/offer/availability/ArbitratorSelectionTest.java b/core/src/test/java/bisq/core/offer/availability/ArbitratorSelectionTest.java index cf22dab40a..9dcf0d4168 100644 --- a/core/src/test/java/bisq/core/offer/availability/ArbitratorSelectionTest.java +++ b/core/src/test/java/bisq/core/offer/availability/ArbitratorSelectionTest.java @@ -36,50 +36,50 @@ public class ArbitratorSelectionTest { lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb1"); arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2")); - result = ArbitratorSelection.getLeastUsedArbitrator(lastAddressesUsedInTrades, arbitrators); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb2", result); // if all are same we use first according to alphanumeric sorting lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb3"); arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2", "arb3")); - result = ArbitratorSelection.getLeastUsedArbitrator(lastAddressesUsedInTrades, arbitrators); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb1", result); lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb3", "arb1"); arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2", "arb3")); - result = ArbitratorSelection.getLeastUsedArbitrator(lastAddressesUsedInTrades, arbitrators); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb2", result); lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb3", "arb1", "arb2"); arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2", "arb3")); - result = ArbitratorSelection.getLeastUsedArbitrator(lastAddressesUsedInTrades, arbitrators); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb3", result); lastAddressesUsedInTrades = Arrays.asList("xxx", "ccc", "aaa"); arbitrators = new HashSet<>(Arrays.asList("aaa", "ccc", "xxx")); - result = ArbitratorSelection.getLeastUsedArbitrator(lastAddressesUsedInTrades, arbitrators); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("aaa", result); lastAddressesUsedInTrades = Arrays.asList("333", "000", "111"); arbitrators = new HashSet<>(Arrays.asList("111", "333", "000")); - result = ArbitratorSelection.getLeastUsedArbitrator(lastAddressesUsedInTrades, arbitrators); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("000", result); // if winner is not in our arb list we use our arb from arbitrators even if never used in trades lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb3"); arbitrators = new HashSet<>(Arrays.asList("arb4")); - result = ArbitratorSelection.getLeastUsedArbitrator(lastAddressesUsedInTrades, arbitrators); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb4", result); // if winner (arb2) is not in our arb list we use our arb from arbitrators lastAddressesUsedInTrades = Arrays.asList("arb1", "arb1", "arb1", "arb2"); arbitrators = new HashSet<>(Arrays.asList("arb1")); - result = ArbitratorSelection.getLeastUsedArbitrator(lastAddressesUsedInTrades, arbitrators); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb1", result); // arb1 is used least lastAddressesUsedInTrades = Arrays.asList("arb1", "arb2", "arb2", "arb2", "arb1", "arb1", "arb2"); arbitrators = new HashSet<>(Arrays.asList("arb1", "arb2")); - result = ArbitratorSelection.getLeastUsedArbitrator(lastAddressesUsedInTrades, arbitrators); + result = DisputeAgentSelection.getLeastUsedDisputeAgent(lastAddressesUsedInTrades, arbitrators); assertEquals("arb1", result); } } diff --git a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java index ba4fc13f3c..9062f5209c 100644 --- a/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java +++ b/core/src/test/java/bisq/core/user/UserPayloadModelVOTest.java @@ -41,9 +41,23 @@ public class UserPayloadModelVOTest { UserPayload vo = new UserPayload(); vo.setAccountId("accountId"); vo.setDisplayedAlert(new Alert("message", true, "version", new byte[]{12, -64, 12}, "string", null)); - vo.setDevelopersFilter(new Filter(Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), - Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), Lists.newArrayList(), - false, Lists.newArrayList(), false, null, null, "string", new byte[]{10, 0, 0}, null)); + vo.setDevelopersFilter(new Filter(Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + Lists.newArrayList(), + false, + Lists.newArrayList(), + false, + null, + null, + "string", + new byte[]{10, 0, 0}, + null, + Lists.newArrayList())); vo.setRegisteredArbitrator(ArbitratorTest.getArbitratorMock()); vo.setRegisteredMediator(MediatorTest.getMediatorMock()); vo.setAcceptedArbitrators(Lists.newArrayList(ArbitratorTest.getArbitratorMock())); diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralAccountNumberForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralAccountNumberForm.java index 7cf16a67cb..07a69a5673 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralAccountNumberForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/GeneralAccountNumberForm.java @@ -18,7 +18,7 @@ import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addInputTextField; import static bisq.desktop.util.FormBuilder.addTopLabelTextField; -abstract public class GeneralAccountNumberForm extends PaymentMethodForm { +public abstract class GeneralAccountNumberForm extends PaymentMethodForm { private InputTextField accountNrInputTextField; diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java index 2f962975e7..ef35d49bb2 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/PaymentMethodForm.java @@ -278,11 +278,11 @@ public abstract class PaymentMethodForm { flowPane.getChildren().add(checkBox); } - abstract protected void autoFillNameTextField(); + protected abstract void autoFillNameTextField(); - abstract public void addFormForAddAccount(); + public abstract void addFormForAddAccount(); - abstract public void addFormForDisplayAccount(); + public abstract void addFormForDisplayAccount(); protected abstract void updateAllInputsValid(); diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.java b/desktop/src/main/java/bisq/desktop/main/MainView.java index d578150e99..e428ef01ce 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainView.java +++ b/desktop/src/main/java/bisq/desktop/main/MainView.java @@ -29,7 +29,6 @@ import bisq.desktop.components.AutoTooltipToggleButton; import bisq.desktop.components.BusyAnimation; import bisq.desktop.main.account.AccountView; import bisq.desktop.main.dao.DaoView; -import bisq.desktop.main.disputes.DisputesView; import bisq.desktop.main.funds.FundsView; import bisq.desktop.main.market.MarketView; import bisq.desktop.main.offer.BuyOfferView; @@ -37,6 +36,8 @@ import bisq.desktop.main.offer.SellOfferView; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.portfolio.PortfolioView; import bisq.desktop.main.settings.SettingsView; +import bisq.desktop.main.shared.PriceFeedComboBoxItem; +import bisq.desktop.main.support.SupportView; import bisq.desktop.util.Transitions; import bisq.core.dao.monitoring.DaoStateMonitoringService; @@ -179,13 +180,13 @@ public class MainView extends InitializableView ToggleButton portfolioButton = new NavButton(PortfolioView.class, Res.get("mainView.menu.portfolio").toUpperCase()); ToggleButton fundsButton = new NavButton(FundsView.class, Res.get("mainView.menu.funds").toUpperCase()); - ToggleButton disputesButton = new NavButton(DisputesView.class, Res.get("mainView.menu.support")); + ToggleButton supportButton = new NavButton(SupportView.class, Res.get("mainView.menu.support")); ToggleButton settingsButton = new NavButton(SettingsView.class, Res.get("mainView.menu.settings")); ToggleButton accountButton = new NavButton(AccountView.class, Res.get("mainView.menu.account")); ToggleButton daoButton = new NavButton(DaoView.class, Res.get("mainView.menu.dao")); JFXBadge portfolioButtonWithBadge = new JFXBadge(portfolioButton); - JFXBadge disputesButtonWithBadge = new JFXBadge(disputesButton); + JFXBadge supportButtonWithBadge = new JFXBadge(supportButton); JFXBadge daoButtonWithBadge = new JFXBadge(daoButton); daoButtonWithBadge.getStyleClass().add("new"); @@ -208,7 +209,7 @@ public class MainView extends InitializableView } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT5, keyEvent)) { fundsButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT6, keyEvent)) { - disputesButton.fire(); + supportButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT7, keyEvent)) { settingsButton.fire(); } else if (Utilities.isAltOrCtrlPressed(KeyCode.DIGIT8, keyEvent)) { @@ -315,7 +316,7 @@ public class MainView extends InitializableView primaryNav.getStyleClass().add("nav-primary"); HBox.setHgrow(primaryNav, Priority.SOMETIMES); - HBox secondaryNav = new HBox(disputesButtonWithBadge, getNavigationSpacer(), settingsButton, + HBox secondaryNav = new HBox(supportButtonWithBadge, getNavigationSpacer(), settingsButton, getNavigationSpacer(), accountButton, getNavigationSpacer(), daoButtonWithBadge); secondaryNav.getStyleClass().add("nav-secondary"); HBox.setHgrow(secondaryNav, Priority.SOMETIMES); @@ -358,7 +359,7 @@ public class MainView extends InitializableView baseApplicationContainer.setBottom(createFooter()); setupBadge(portfolioButtonWithBadge, model.getNumPendingTrades(), model.getShowPendingTradesNotification()); - setupBadge(disputesButtonWithBadge, model.getNumOpenDisputes(), model.getShowOpenDisputesNotification()); + setupBadge(supportButtonWithBadge, model.getNumOpenSupportTickets(), model.getShowOpenSupportTicketsNotification()); setupBadge(daoButtonWithBadge, new SimpleStringProperty(Res.get("shared.new")), model.getShowDaoUpdatesNotification()); navigation.addListener(viewPath -> { diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index 61aefb01d4..e35b5bc55c 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -32,6 +32,7 @@ import bisq.desktop.main.overlays.windows.WalletPasswordWindow; import bisq.desktop.main.overlays.windows.downloadupdate.DisplayUpdateDownloadWindow; import bisq.desktop.main.presentation.DaoPresentation; import bisq.desktop.main.presentation.MarketPricePresentation; +import bisq.desktop.main.shared.PriceFeedComboBoxItem; import bisq.desktop.util.GUIUtil; import bisq.core.account.witness.AccountAgeWitnessService; @@ -46,7 +47,7 @@ import bisq.core.locale.Res; import bisq.core.payment.AliPayAccount; import bisq.core.payment.CryptoCurrencyAccount; import bisq.core.presentation.BalancePresentation; -import bisq.core.presentation.DisputePresentation; +import bisq.core.presentation.SupportTicketsPresentation; import bisq.core.presentation.TradePresentation; import bisq.core.provider.fee.FeeService; import bisq.core.provider.price.PriceFeedService; @@ -99,7 +100,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupCompleteList private final User user; private final BalancePresentation balancePresentation; private final TradePresentation tradePresentation; - private final DisputePresentation disputePresentation; + private final SupportTicketsPresentation supportTicketsPresentation; private final MarketPricePresentation marketPricePresentation; private final DaoPresentation daoPresentation; private final P2PService p2PService; @@ -134,7 +135,6 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupCompleteList // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - @SuppressWarnings("WeakerAccess") @Inject public MainViewModel(BisqSetup bisqSetup, WalletsSetup walletsSetup, @@ -142,7 +142,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupCompleteList User user, BalancePresentation balancePresentation, TradePresentation tradePresentation, - DisputePresentation disputePresentation, + SupportTicketsPresentation supportTicketsPresentation, MarketPricePresentation marketPricePresentation, DaoPresentation daoPresentation, P2PService p2PService, @@ -164,7 +164,7 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupCompleteList this.user = user; this.balancePresentation = balancePresentation; this.tradePresentation = tradePresentation; - this.disputePresentation = disputePresentation; + this.supportTicketsPresentation = supportTicketsPresentation; this.marketPricePresentation = marketPricePresentation; this.daoPresentation = daoPresentation; this.p2PService = p2PService; @@ -506,12 +506,12 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupCompleteList return bisqSetup.getNewVersionAvailableProperty(); } - StringProperty getNumOpenDisputes() { - return disputePresentation.getNumOpenDisputes(); + StringProperty getNumOpenSupportTickets() { + return supportTicketsPresentation.getNumOpenSupportTickets(); } - BooleanProperty getShowOpenDisputesNotification() { - return disputePresentation.getShowOpenDisputesNotification(); + BooleanProperty getShowOpenSupportTicketsNotification() { + return supportTicketsPresentation.getShowOpenSupportTicketsNotification(); } BooleanProperty getShowPendingTradesNotification() { diff --git a/desktop/src/main/java/bisq/desktop/main/account/AccountView.java b/desktop/src/main/java/bisq/desktop/main/account/AccountView.java index 6e880fd65e..0c9dc8c8e2 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/AccountView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/AccountView.java @@ -24,13 +24,14 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.common.view.View; import bisq.desktop.common.view.ViewLoader; import bisq.desktop.main.MainView; -import bisq.desktop.main.account.arbitratorregistration.ArbitratorRegistrationView; import bisq.desktop.main.account.content.altcoinaccounts.AltCoinAccountsView; import bisq.desktop.main.account.content.backup.BackupView; import bisq.desktop.main.account.content.fiataccounts.FiatAccountsView; import bisq.desktop.main.account.content.notifications.MobileNotificationsView; import bisq.desktop.main.account.content.password.PasswordView; import bisq.desktop.main.account.content.seedwords.SeedWordsView; +import bisq.desktop.main.account.register.arbitrator.ArbitratorRegistrationView; +import bisq.desktop.main.account.register.mediator.MediatorRegistrationView; import bisq.desktop.main.overlays.popups.Popup; import bisq.core.locale.Res; @@ -71,7 +72,9 @@ public class AccountView extends ActivatableView { private final Navigation navigation; private Tab selectedTab; private Tab arbitratorRegistrationTab; + private Tab mediatorRegistrationTab; private ArbitratorRegistrationView arbitratorRegistrationView; + private MediatorRegistrationView mediatorRegistrationView; private Scene scene; private EventHandler keyEventEventHandler; private ListChangeListener tabListChangeListener; @@ -96,29 +99,43 @@ public class AccountView extends ActivatableView { navigationListener = viewPath -> { if (viewPath.size() == 3 && viewPath.indexOf(AccountView.class) == 1) { - if (arbitratorRegistrationTab == null && viewPath.get(2).equals(ArbitratorRegistrationView.class)) + if (arbitratorRegistrationTab == null && viewPath.get(2).equals(ArbitratorRegistrationView.class)) { navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); - else + } else if (mediatorRegistrationTab == null && viewPath.get(2).equals(MediatorRegistrationView.class)) { + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } else { loadView(viewPath.tip()); + } } else { resetSelectedTab(); } }; keyEventEventHandler = event -> { - if (Utilities.isAltOrCtrlPressed(KeyCode.R, event) && - arbitratorRegistrationTab == null) { + if (Utilities.isAltOrCtrlPressed(KeyCode.R, event) && arbitratorRegistrationTab == null) { + if (mediatorRegistrationTab != null) { + root.getTabs().remove(mediatorRegistrationTab); + } arbitratorRegistrationTab = new Tab(Res.get("account.tab.arbitratorRegistration").toUpperCase()); arbitratorRegistrationTab.setClosable(true); root.getTabs().add(arbitratorRegistrationTab); - navigation.navigateTo(MainView.class, AccountView.class, ArbitratorRegistrationView.class); + } else if (Utilities.isAltOrCtrlPressed(KeyCode.D, event) && mediatorRegistrationTab == null) { + if (arbitratorRegistrationTab != null) { + root.getTabs().remove(arbitratorRegistrationTab); + } + mediatorRegistrationTab = new Tab(Res.get("account.tab.mediatorRegistration").toUpperCase()); + mediatorRegistrationTab.setClosable(true); + root.getTabs().add(mediatorRegistrationTab); + navigation.navigateTo(MainView.class, AccountView.class, MediatorRegistrationView.class); } }; tabChangeListener = (ov, oldValue, newValue) -> { if (arbitratorRegistrationTab != null && selectedTab != arbitratorRegistrationTab) { navigation.navigateTo(MainView.class, AccountView.class, ArbitratorRegistrationView.class); + } else if (mediatorRegistrationTab != null && selectedTab != mediatorRegistrationTab) { + navigation.navigateTo(MainView.class, AccountView.class, MediatorRegistrationView.class); } else if (newValue == fiatAccountsTab && selectedTab != fiatAccountsTab) { navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); } else if (newValue == altcoinAccountsTab && selectedTab != altcoinAccountsTab) { @@ -139,16 +156,22 @@ public class AccountView extends ActivatableView { List removedTabs = change.getRemoved(); if (removedTabs.size() == 1 && removedTabs.get(0).equals(arbitratorRegistrationTab)) onArbitratorRegistrationTabRemoved(); + + if (removedTabs.size() == 1 && removedTabs.get(0).equals(mediatorRegistrationTab)) + onMediatorRegistrationTabRemoved(); }; } private void onArbitratorRegistrationTabRemoved() { arbitratorRegistrationTab = null; - navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); } - @SuppressWarnings("PointlessBooleanExpression") + private void onMediatorRegistrationTabRemoved() { + mediatorRegistrationTab = null; + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + } + @Override protected void activate() { navigation.addListener(navigationListener); @@ -163,6 +186,8 @@ public class AccountView extends ActivatableView { if (navigation.getCurrentPath().size() == 2 && navigation.getCurrentPath().get(1) == AccountView.class) { if (arbitratorRegistrationTab != null) navigation.navigateTo(MainView.class, AccountView.class, ArbitratorRegistrationView.class); + else if (mediatorRegistrationTab != null) + navigation.navigateTo(MainView.class, AccountView.class, MediatorRegistrationView.class); else if (root.getSelectionModel().getSelectedItem() == fiatAccountsTab) navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); else if (root.getSelectionModel().getSelectedItem() == altcoinAccountsTab) @@ -179,7 +204,6 @@ public class AccountView extends ActivatableView { navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); } - //noinspection UnusedAssignment String key = "accountPrivacyInfo"; if (!DevEnv.isDevMode()) new Popup<>() @@ -210,6 +234,12 @@ public class AccountView extends ActivatableView { arbitratorRegistrationView = (ArbitratorRegistrationView) view; arbitratorRegistrationView.onTabSelection(true); } + } else if (view instanceof MediatorRegistrationView) { + if (mediatorRegistrationTab != null) { + selectedTab = mediatorRegistrationTab; + mediatorRegistrationView = (MediatorRegistrationView) view; + mediatorRegistrationView.onTabSelection(true); + } } else if (view instanceof FiatAccountsView) { selectedTab = fiatAccountsTab; } else if (view instanceof AltCoinAccountsView) { diff --git a/desktop/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationView.java b/desktop/src/main/java/bisq/desktop/main/account/register/AgentRegistrationView.java similarity index 84% rename from desktop/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationView.java rename to desktop/src/main/java/bisq/desktop/main/account/register/AgentRegistrationView.java index 307fbe46dc..5901ad6f03 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/register/AgentRegistrationView.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main.account.arbitratorregistration; +package bisq.desktop.main.account.register; import bisq.desktop.common.view.ActivatableViewAndModel; @@ -24,13 +24,12 @@ import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.TitledGroupBg; import bisq.desktop.main.overlays.popups.Popup; -import bisq.desktop.main.overlays.windows.UnlockArbitrationRegistrationWindow; +import bisq.desktop.main.overlays.windows.UnlockDisputeAgentRegistrationWindow; import bisq.desktop.util.FormBuilder; import bisq.desktop.util.ImageUtil; import bisq.desktop.util.Layout; import bisq.core.app.AppOptionKeys; -import bisq.core.arbitration.Arbitrator; import bisq.core.locale.LanguageUtil; import bisq.core.locale.Res; @@ -40,8 +39,6 @@ import bisq.common.util.Tuple3; import com.google.inject.name.Named; -import javax.inject.Inject; - import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; @@ -70,8 +67,10 @@ import static bisq.desktop.util.FormBuilder.addMultilineLabel; import static bisq.desktop.util.FormBuilder.addTitledGroupBg; import static bisq.desktop.util.FormBuilder.addTopLabelTextField; +// TODO translation string keys should renamed to be more generic. +// Lets do it for 1.1.7 the translator have time to add new string. @FxmlView -public class ArbitratorRegistrationView extends ActivatableViewAndModel { +public abstract class AgentRegistrationView extends ActivatableViewAndModel { private final boolean useDevPrivilegeKeys; private ListView languagesListView; @@ -79,8 +78,8 @@ public class ArbitratorRegistrationView extends ActivatableViewAndModel arbitratorChangeListener; - private UnlockArbitrationRegistrationWindow unlockArbitrationRegistrationWindow; + private ChangeListener changeListener; + private UnlockDisputeAgentRegistrationWindow unlockDisputeAgentRegistrationWindow; private ListChangeListener listChangeListener; @@ -88,8 +87,7 @@ public class ArbitratorRegistrationView extends ActivatableViewAndModel updateLanguageList(); + changeListener = (observable, oldValue, newValue) -> updateLanguageList(); } @Override @@ -109,24 +107,24 @@ public class ArbitratorRegistrationView extends ActivatableViewAndModel unlockArbitrationRegistrationWindow = null) + if (model.registrationPubKeyAsHex.get() == null && unlockDisputeAgentRegistrationWindow == null) { + unlockDisputeAgentRegistrationWindow = new UnlockDisputeAgentRegistrationWindow(useDevPrivilegeKeys); + unlockDisputeAgentRegistrationWindow.onClose(() -> unlockDisputeAgentRegistrationWindow = null) .onKey(model::setPrivKeyAndCheckPubKey) .width(700) .show(); } } else { - model.myArbitratorProperty.removeListener(arbitratorChangeListener); + model.myDisputeAgentProperty.removeListener(changeListener); } } @@ -149,7 +147,7 @@ public class ArbitratorRegistrationView extends ActivatableViewAndModel onAddLanguage()); - final Tuple2 buttonButtonTuple2 = add2ButtonsAfterGroup(gridPane, ++gridRow, Res.get("account.arbitratorRegistration.register"), Res.get("account.arbitratorRegistration.revoke")); + Tuple2 buttonButtonTuple2 = add2ButtonsAfterGroup(gridPane, ++gridRow, + Res.get("account.arbitratorRegistration.register"), Res.get("account.arbitratorRegistration.revoke")); Button registerButton = buttonButtonTuple2.first; registerButton.disableProperty().bind(model.registrationEditDisabled); registerButton.setOnAction(e -> onRegister()); @@ -224,9 +223,12 @@ public class ArbitratorRegistrationView extends ActivatableViewAndModel new Popup<>().feedback(Res.get("account.arbitratorRegistration.removedSuccess")).show(), (errorMessage) -> new Popup<>().error(Res.get("account.arbitratorRegistration.removedFailed", Res.get("shared.errorMessageInline", errorMessage))).show()); - } else { - new Popup<>().information(Res.get("popup.warning.notFullyConnected")).show(); } } private void onRegister() { - if (model.isBootstrapped()) { + if (model.isBootstrappedOrShowPopup()) { model.onRegister( () -> new Popup<>().feedback(Res.get("account.arbitratorRegistration.registerSuccess")).show(), (errorMessage) -> new Popup<>().error(Res.get("account.arbitratorRegistration.registerFailed", Res.get("shared.errorMessageInline", errorMessage))).show()); - } else { - new Popup<>().information(Res.get("popup.warning.notFullyConnected")).show(); } } } diff --git a/desktop/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationViewModel.java b/desktop/src/main/java/bisq/desktop/main/account/register/AgentRegistrationViewModel.java similarity index 55% rename from desktop/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationViewModel.java rename to desktop/src/main/java/bisq/desktop/main/account/register/AgentRegistrationViewModel.java index b7bdfa72a3..602b66949a 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/account/register/AgentRegistrationViewModel.java @@ -15,15 +15,15 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main.account.arbitratorregistration; +package bisq.desktop.main.account.register; import bisq.desktop.common.model.ActivatableViewModel; +import bisq.desktop.util.GUIUtil; -import bisq.core.arbitration.Arbitrator; -import bisq.core.arbitration.ArbitratorManager; -import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.locale.LanguageUtil; +import bisq.core.support.dispute.agent.DisputeAgent; +import bisq.core.support.dispute.agent.DisputeAgentManager; import bisq.core.user.User; import bisq.network.p2p.NodeAddress; @@ -36,8 +36,6 @@ import bisq.common.handlers.ResultHandler; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.Utils; -import com.google.inject.Inject; - import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -49,25 +47,22 @@ import javafx.collections.FXCollections; import javafx.collections.MapChangeListener; import javafx.collections.ObservableList; -import java.util.ArrayList; -import java.util.Date; - -class ArbitratorRegistrationViewModel extends ActivatableViewModel { - private final ArbitratorManager arbitratorManager; - private final User user; - private final P2PService p2PService; - private final BtcWalletService walletService; - private final KeyRing keyRing; +public abstract class AgentRegistrationViewModel> extends ActivatableViewModel { + private final T disputeAgentManager; + protected final User user; + protected final P2PService p2PService; + protected final BtcWalletService walletService; + protected final KeyRing keyRing; final BooleanProperty registrationEditDisabled = new SimpleBooleanProperty(true); final BooleanProperty revokeButtonDisabled = new SimpleBooleanProperty(true); - final ObjectProperty myArbitratorProperty = new SimpleObjectProperty<>(); + final ObjectProperty myDisputeAgentProperty = new SimpleObjectProperty<>(); - final ObservableList languageCodes = FXCollections.observableArrayList(LanguageUtil.getDefaultLanguageLocaleAsCode()); + protected final ObservableList languageCodes = FXCollections.observableArrayList(LanguageUtil.getDefaultLanguageLocaleAsCode()); final ObservableList allLanguageCodes = FXCollections.observableArrayList(LanguageUtil.getAllLanguageCodes()); private boolean allDataValid; - private final MapChangeListener arbitratorMapChangeListener; - private ECKey registrationKey; + private final MapChangeListener mapChangeListener; + protected ECKey registrationKey; final StringProperty registrationPubKeyAsHex = new SimpleStringProperty(); @@ -75,45 +70,42 @@ class ArbitratorRegistrationViewModel extends ActivatableViewModel { // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// - @Inject - public ArbitratorRegistrationViewModel(ArbitratorManager arbitratorManager, - User user, - P2PService p2PService, - BtcWalletService walletService, - KeyRing keyRing) { - this.arbitratorManager = arbitratorManager; + public AgentRegistrationViewModel(T disputeAgentManager, + User user, + P2PService p2PService, + BtcWalletService walletService, + KeyRing keyRing) { + this.disputeAgentManager = disputeAgentManager; this.user = user; this.p2PService = p2PService; this.walletService = walletService; this.keyRing = keyRing; - arbitratorMapChangeListener = new MapChangeListener() { - @Override - public void onChanged(Change change) { - Arbitrator myRegisteredArbitrator = user.getRegisteredArbitrator(); - myArbitratorProperty.set(myRegisteredArbitrator); + mapChangeListener = change -> { + R registeredDisputeAgentFromUser = getRegisteredDisputeAgentFromUser(); + myDisputeAgentProperty.set(registeredDisputeAgentFromUser); - // We don't reset the languages in case of revocation, as its likely that the arbitrator will use the same again when he re-activate - // registration later - if (myRegisteredArbitrator != null) - languageCodes.setAll(myRegisteredArbitrator.getLanguageCodes()); + // We don't reset the languages in case of revocation, as its likely that the disputeAgent will use the + // same again when he re-activate registration later + if (registeredDisputeAgentFromUser != null) + languageCodes.setAll(registeredDisputeAgentFromUser.getLanguageCodes()); - updateDisableStates(); - } + updateDisableStates(); }; } @Override protected void activate() { - arbitratorManager.getArbitratorsObservableMap().addListener(arbitratorMapChangeListener); - Arbitrator myRegisteredArbitrator = user.getRegisteredArbitrator(); - myArbitratorProperty.set(myRegisteredArbitrator); + disputeAgentManager.getObservableMap().addListener(mapChangeListener); + myDisputeAgentProperty.set(getRegisteredDisputeAgentFromUser()); updateDisableStates(); } + protected abstract R getRegisteredDisputeAgentFromUser(); + @Override protected void deactivate() { - arbitratorManager.getArbitratorsObservableMap().removeListener(arbitratorMapChangeListener); + disputeAgentManager.getObservableMap().removeListener(mapChangeListener); } @@ -136,10 +128,10 @@ class ArbitratorRegistrationViewModel extends ActivatableViewModel { } boolean setPrivKeyAndCheckPubKey(String privKeyString) { - ECKey registrationKey = arbitratorManager.getRegistrationKey(privKeyString); + ECKey registrationKey = disputeAgentManager.getRegistrationKey(privKeyString); if (registrationKey != null) { String _registrationPubKeyAsHex = Utils.HEX.encode(registrationKey.getPubKey()); - boolean isKeyValid = arbitratorManager.isPublicKeyInList(_registrationPubKeyAsHex); + boolean isKeyValid = disputeAgentManager.isPublicKeyInList(_registrationPubKeyAsHex); if (isKeyValid) { this.registrationKey = registrationKey; registrationPubKeyAsHex.set(_registrationPubKeyAsHex); @@ -152,29 +144,18 @@ class ArbitratorRegistrationViewModel extends ActivatableViewModel { } } + protected abstract R getDisputeAgent(String registrationSignature, String emailAddress); + void onRegister(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { updateDisableStates(); if (allDataValid) { - AddressEntry arbitratorDepositAddressEntry = walletService.getArbitratorAddressEntry(); - String registrationSignature = arbitratorManager.signStorageSignaturePubKey(registrationKey); + String registrationSignature = disputeAgentManager.signStorageSignaturePubKey(registrationKey); // TODO not impl in UI String emailAddress = null; @SuppressWarnings("ConstantConditions") - Arbitrator arbitrator = new Arbitrator( - p2PService.getAddress(), - arbitratorDepositAddressEntry.getPubKey(), - arbitratorDepositAddressEntry.getAddressString(), - keyRing.getPubKeyRing(), - new ArrayList<>(languageCodes), - new Date().getTime(), - registrationKey.getPubKey(), - registrationSignature, - emailAddress, - null, - null - ); + R disputeAgent = getDisputeAgent(registrationSignature, emailAddress); - arbitratorManager.addArbitrator(arbitrator, + disputeAgentManager.addDisputeAgent(disputeAgent, () -> { updateDisableStates(); resultHandler.handleResult(); @@ -186,9 +167,8 @@ class ArbitratorRegistrationViewModel extends ActivatableViewModel { } } - void onRevoke(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - arbitratorManager.removeArbitrator( + disputeAgentManager.removeDisputeAgent( () -> { updateDisableStates(); resultHandler.handleResult(); @@ -200,12 +180,12 @@ class ArbitratorRegistrationViewModel extends ActivatableViewModel { } private void updateDisableStates() { - allDataValid = languageCodes != null && languageCodes.size() > 0 && registrationKey != null && registrationPubKeyAsHex.get() != null; - registrationEditDisabled.set(!allDataValid || myArbitratorProperty.get() != null); - revokeButtonDisabled.set(!allDataValid || myArbitratorProperty.get() == null); + allDataValid = languageCodes.size() > 0 && registrationKey != null && registrationPubKeyAsHex.get() != null; + registrationEditDisabled.set(!allDataValid || myDisputeAgentProperty.get() != null); + revokeButtonDisabled.set(!allDataValid || myDisputeAgentProperty.get() == null); } - boolean isBootstrapped() { - return p2PService.isBootstrapped(); + boolean isBootstrappedOrShowPopup() { + return GUIUtil.isBootstrappedOrShowPopup(p2PService); } } diff --git a/desktop/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationView.fxml b/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.fxml similarity index 89% rename from desktop/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationView.fxml rename to desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.fxml index c78f240787..fbbe07b6ab 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/arbitratorregistration/ArbitratorRegistrationView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.fxml @@ -18,7 +18,7 @@ - diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.java b/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.java new file mode 100644 index 0000000000..713ebe5379 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationView.java @@ -0,0 +1,45 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.main.account.register.arbitrator; + + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.account.register.AgentRegistrationView; + +import bisq.core.app.AppOptionKeys; +import bisq.core.locale.Res; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; + +import com.google.inject.name.Named; + +import javax.inject.Inject; + +@FxmlView +public class ArbitratorRegistrationView extends AgentRegistrationView { + + @Inject + public ArbitratorRegistrationView(ArbitratorRegistrationViewModel model, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(model, useDevPrivilegeKeys); + } + + @Override + protected String getRole() { + return Res.get("shared.arbitrator2"); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationViewModel.java b/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationViewModel.java new file mode 100644 index 0000000000..ec14062884 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/arbitrator/ArbitratorRegistrationViewModel.java @@ -0,0 +1,71 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.main.account.register.arbitrator; + +import bisq.desktop.main.account.register.AgentRegistrationViewModel; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.user.User; + +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.Inject; + +import java.util.ArrayList; +import java.util.Date; + +public class ArbitratorRegistrationViewModel extends AgentRegistrationViewModel { + + @Inject + public ArbitratorRegistrationViewModel(ArbitratorManager arbitratorManager, + User user, + P2PService p2PService, + BtcWalletService walletService, + KeyRing keyRing) { + super(arbitratorManager, user, p2PService, walletService, keyRing); + } + + @Override + protected Arbitrator getDisputeAgent(String registrationSignature, + String emailAddress) { + AddressEntry arbitratorAddressEntry = walletService.getArbitratorAddressEntry(); + return new Arbitrator( + p2PService.getAddress(), + arbitratorAddressEntry.getPubKey(), + arbitratorAddressEntry.getAddressString(), + keyRing.getPubKeyRing(), + new ArrayList<>(languageCodes), + new Date().getTime(), + registrationKey.getPubKey(), + registrationSignature, + emailAddress, + null, + null + ); + } + + @Override + protected Arbitrator getRegisteredDisputeAgentFromUser() { + return user.getRegisteredArbitrator(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.fxml b/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.fxml new file mode 100644 index 0000000000..0d180bee04 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.fxml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.java b/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.java new file mode 100644 index 0000000000..4a0ac30f96 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationView.java @@ -0,0 +1,45 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.main.account.register.mediator; + + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.account.register.AgentRegistrationView; + +import bisq.core.app.AppOptionKeys; +import bisq.core.locale.Res; +import bisq.core.support.dispute.mediation.mediator.Mediator; + +import com.google.inject.name.Named; + +import javax.inject.Inject; + +@FxmlView +public class MediatorRegistrationView extends AgentRegistrationView { + + @Inject + public MediatorRegistrationView(MediatorRegistrationViewModel model, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(model, useDevPrivilegeKeys); + } + + @Override + protected String getRole() { + return Res.get("shared.mediator"); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationViewModel.java b/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationViewModel.java new file mode 100644 index 0000000000..d763c80261 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/account/register/mediator/MediatorRegistrationViewModel.java @@ -0,0 +1,67 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.main.account.register.mediator; + +import bisq.desktop.main.account.register.AgentRegistrationViewModel; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.user.User; + +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.Inject; + +import java.util.ArrayList; +import java.util.Date; + +class MediatorRegistrationViewModel extends AgentRegistrationViewModel { + + @Inject + public MediatorRegistrationViewModel(MediatorManager mediatorManager, + User user, + P2PService p2PService, + BtcWalletService walletService, + KeyRing keyRing) { + super(mediatorManager, user, p2PService, walletService, keyRing); + } + + @Override + protected Mediator getDisputeAgent(String registrationSignature, + String emailAddress) { + return new Mediator( + p2PService.getAddress(), + keyRing.getPubKeyRing(), + new ArrayList<>(languageCodes), + new Date().getTime(), + registrationKey.getPubKey(), + registrationSignature, + emailAddress, + null, + null + ); + } + + @Override + protected Mediator getRegisteredDisputeAgentFromUser() { + return user.getRegisteredMediator(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java b/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java index bd0a998099..38bef6c897 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/bonding/BondingViewUtils.java @@ -106,7 +106,7 @@ public class BondingViewUtils { private void lockupBond(byte[] hash, Coin lockupAmount, int lockupTime, LockupReason lockupReason, Consumer resultHandler) { - if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { if (!DevEnv.isDevMode()) { try { Tuple2 miningFeeAndTxSize = daoFacade.getLockupTxMiningFeeAndTxSize(lockupAmount, lockupTime, lockupReason, hash); @@ -135,8 +135,6 @@ public class BondingViewUtils { } else { publishLockupTx(lockupAmount, lockupTime, lockupReason, hash, resultHandler); } - } else { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); } } @@ -161,7 +159,7 @@ public class BondingViewUtils { } public void unLock(String lockupTxId, Consumer resultHandler) { - if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { Optional lockupTxOutput = daoFacade.getLockupTxOutput(lockupTxId); checkArgument(lockupTxOutput.isPresent(), "Lockup output must be present. TxId=" + lockupTxId); Coin unlockAmount = Coin.valueOf(lockupTxOutput.get().getValue()); @@ -196,8 +194,6 @@ public class BondingViewUtils { t.printStackTrace(); new Popup<>().warning(t.getMessage()).show(); } - } else { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); } log.info("unlock tx: {}", lockupTxId); } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java index 4219d86c40..c7b41c873f 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java @@ -404,9 +404,9 @@ public class MakeProposalView extends ActivatableView implements bsqFormatter.parseToCoin(proposalDisplay.requestedBsqTextField.getText())); case CHANGE_PARAM: checkNotNull(proposalDisplay.paramComboBox, - "proposalDisplay.paramComboBox must no tbe null"); + "proposalDisplay.paramComboBox must not be null"); checkNotNull(proposalDisplay.paramValueTextField, - "proposalDisplay.paramValueTextField must no tbe null"); + "proposalDisplay.paramValueTextField must not be null"); Param selectedParam = proposalDisplay.paramComboBox.getSelectionModel().getSelectedItem(); if (selectedParam == null) throw new ProposalValidationException("selectedParam is null"); @@ -492,10 +492,8 @@ public class MakeProposalView extends ActivatableView implements private void setMakeProposalButtonHandler() { makeProposalButton.setOnAction(event -> { - if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { publishMyProposal(selectedProposalType); - } else { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); } }); } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java index 24de472f15..2d773a0394 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/send/BsqSendView.java @@ -232,7 +232,7 @@ public class BsqSendView extends ActivatableView implements BsqB sendBsqButton.setOnAction((event) -> { // TODO break up in methods - if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { String receiversAddressString = bsqFormatter.getAddressFromBsqAddress(receiversAddressInputTextField.getText()).toString(); Coin receiverAmount = bsqFormatter.parseToCoin(amountInputTextField.getText()); try { @@ -259,8 +259,6 @@ public class BsqSendView extends ActivatableView implements BsqB } catch (Throwable t) { handleError(t); } - } else { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); } }); } @@ -292,7 +290,7 @@ public class BsqSendView extends ActivatableView implements BsqB sendBtcButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.wallet.send.sendBtc")); sendBtcButton.setOnAction((event) -> { - if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { String receiversAddressString = receiversBtcAddressInputTextField.getText(); Coin receiverAmount = bsqFormatter.parseToBTC(btcAmountInputTextField.getText()); try { @@ -324,8 +322,6 @@ public class BsqSendView extends ActivatableView implements BsqB } catch (Throwable t) { handleError(t); } - } else { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); } }); } diff --git a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java index afa3c68d1a..7bd2b24707 100644 --- a/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java +++ b/desktop/src/main/java/bisq/desktop/main/debug/DebugView.java @@ -50,7 +50,6 @@ import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerCreatesDeposi import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerSignAndPublishDepositTx; import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx; import bisq.core.trade.protocol.tasks.taker.TakerProcessPublishDepositTxRequest; -import bisq.core.trade.protocol.tasks.taker.TakerSelectMediator; import bisq.core.trade.protocol.tasks.taker.TakerSendDepositTxPublishedMessage; import bisq.core.trade.protocol.tasks.taker.TakerSendPayDepositRequest; import bisq.core.trade.protocol.tasks.taker.TakerVerifyAndSignContract; @@ -131,7 +130,6 @@ public class DebugView extends InitializableView { FXCollections.observableArrayList(Arrays.asList( TakerVerifyMakerAccount.class, TakerVerifyMakerFeePayment.class, - TakerSelectMediator.class, CreateTakerFeeTx.class, SellerAsTakerCreatesDepositTxInputs.class, TakerSendPayDepositRequest.class, diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/DisputesView.java b/desktop/src/main/java/bisq/desktop/main/disputes/DisputesView.java deleted file mode 100644 index 48864252d3..0000000000 --- a/desktop/src/main/java/bisq/desktop/main/disputes/DisputesView.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq 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. - * - * Bisq 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 Bisq. If not, see . - */ - -package bisq.desktop.main.disputes; - -import bisq.desktop.Navigation; -import bisq.desktop.common.model.Activatable; -import bisq.desktop.common.view.ActivatableViewAndModel; -import bisq.desktop.common.view.CachingViewLoader; -import bisq.desktop.common.view.FxmlView; -import bisq.desktop.common.view.View; -import bisq.desktop.common.view.ViewLoader; -import bisq.desktop.main.MainView; -import bisq.desktop.main.disputes.arbitrator.ArbitratorDisputeView; -import bisq.desktop.main.disputes.trader.TraderDisputeView; -import bisq.desktop.main.overlays.popups.Popup; - -import bisq.core.arbitration.Arbitrator; -import bisq.core.arbitration.ArbitratorManager; -import bisq.core.arbitration.DisputeManager; -import bisq.core.locale.Res; - -import bisq.network.p2p.NodeAddress; - -import bisq.common.app.DevEnv; -import bisq.common.crypto.KeyRing; - -import javax.inject.Inject; - -import javafx.fxml.FXML; - -import javafx.scene.control.Tab; -import javafx.scene.control.TabPane; - -import javafx.beans.value.ChangeListener; - -import javafx.collections.MapChangeListener; - -// will be probably only used for arbitration communication, will be renamed and the icon changed -@FxmlView -public class DisputesView extends ActivatableViewAndModel { - - @FXML - Tab tradersDisputesTab; - - private Tab arbitratorsDisputesTab; - - private final Navigation navigation; - private final ArbitratorManager arbitratorManager; - private final DisputeManager disputeManager; - private final KeyRing keyRing; - - private Navigation.Listener navigationListener; - private ChangeListener tabChangeListener; - private Tab currentTab; - private final ViewLoader viewLoader; - private MapChangeListener arbitratorMapChangeListener; - - @Inject - public DisputesView(CachingViewLoader viewLoader, Navigation navigation, - ArbitratorManager arbitratorManager, DisputeManager disputeManager, - KeyRing keyRing) { - this.viewLoader = viewLoader; - this.navigation = navigation; - this.arbitratorManager = arbitratorManager; - this.disputeManager = disputeManager; - this.keyRing = keyRing; - } - - @Override - public void initialize() { - log.debug("initialize "); - tradersDisputesTab.setText(Res.get("support.tab.support").toUpperCase()); - navigationListener = viewPath -> { - if (viewPath.size() == 3 && viewPath.indexOf(DisputesView.class) == 1) - loadView(viewPath.tip()); - }; - - tabChangeListener = (ov, oldValue, newValue) -> { - if (newValue == tradersDisputesTab) - navigation.navigateTo(MainView.class, DisputesView.class, TraderDisputeView.class); - else if (newValue == arbitratorsDisputesTab) - navigation.navigateTo(MainView.class, DisputesView.class, ArbitratorDisputeView.class); - }; - - arbitratorMapChangeListener = change -> updateArbitratorsDisputesTabDisableState(); - } - - private void updateArbitratorsDisputesTabDisableState() { - boolean isActiveArbitrator = arbitratorManager.getArbitratorsObservableMap().values().stream() - .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(keyRing.getPubKeyRing())); - - boolean hasDisputesAsArbitrator = disputeManager.getDisputesAsObservableList().stream() - .anyMatch(d -> d.getArbitratorPubKeyRing().equals(keyRing.getPubKeyRing())); - - if (arbitratorsDisputesTab == null && (isActiveArbitrator || hasDisputesAsArbitrator)) { - arbitratorsDisputesTab = new Tab(Res.get("support.tab.ArbitratorsSupportTickets").toUpperCase()); - arbitratorsDisputesTab.setClosable(false); - root.getTabs().add(arbitratorsDisputesTab); - tradersDisputesTab.setText(Res.get("support.tab.TradersSupportTickets").toUpperCase()); - } - } - - @SuppressWarnings("PointlessBooleanExpression") - @Override - protected void activate() { - arbitratorManager.updateArbitratorMap(); - arbitratorManager.getArbitratorsObservableMap().addListener(arbitratorMapChangeListener); - updateArbitratorsDisputesTabDisableState(); - - root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); - navigation.addListener(navigationListener); - - if (arbitratorsDisputesTab != null && root.getSelectionModel().getSelectedItem() == arbitratorsDisputesTab) - navigation.navigateTo(MainView.class, DisputesView.class, ArbitratorDisputeView.class); - else - navigation.navigateTo(MainView.class, DisputesView.class, TraderDisputeView.class); - - //noinspection UnusedAssignment - String key = "supportInfo"; - if (!DevEnv.isDevMode()) - new Popup<>().backgroundInfo(Res.get("support.backgroundInfo")) - .width(900) - .dontShowAgainId(key) - .show(); - } - - @Override - protected void deactivate() { - arbitratorManager.getArbitratorsObservableMap().removeListener(arbitratorMapChangeListener); - root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); - navigation.removeListener(navigationListener); - currentTab = null; - } - - private void loadView(Class viewClass) { - // we want to get activate/deactivate called, so we remove the old view on tab change - if (currentTab != null) - currentTab.setContent(null); - - View view = viewLoader.load(viewClass); - - if (arbitratorsDisputesTab != null && view instanceof ArbitratorDisputeView) - currentTab = arbitratorsDisputesTab; - else - currentTab = tradersDisputesTab; - - currentTab.setContent(view.getRoot()); - root.getSelectionModel().select(currentTab); - } -} - diff --git a/desktop/src/main/java/bisq/desktop/main/funds/locked/LockedView.java b/desktop/src/main/java/bisq/desktop/main/funds/locked/LockedView.java index 38275c3ba2..fd2f2e070f 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/locked/LockedView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/locked/LockedView.java @@ -264,8 +264,6 @@ public class LockedView extends ActivatableView { field.setOnAction(event -> openDetailPopup(item)); field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); setGraphic(field); - } else if (addressEntry.getContext() == AddressEntry.Context.ARBITRATOR) { - setGraphic(new AutoTooltipLabel(Res.get("shared.arbitratorsFee"))); } else { setGraphic(new AutoTooltipLabel(Res.get("shared.noDetailsAvailable"))); } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/reserved/ReservedView.java b/desktop/src/main/java/bisq/desktop/main/funds/reserved/ReservedView.java index 0146d9c14f..9efb8da1e5 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/reserved/ReservedView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/reserved/ReservedView.java @@ -263,8 +263,6 @@ public class ReservedView extends ActivatableView { field.setOnAction(event -> openDetailPopup(item)); field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); setGraphic(field); - } else if (item.getAddressEntry().getContext() == AddressEntry.Context.ARBITRATOR) { - setGraphic(new AutoTooltipLabel(Res.get("shared.arbitratorsFee"))); } else { setGraphic(new AutoTooltipLabel(Res.get("shared.noDetailsAvailable"))); } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactory.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactory.java index 234d96c6b3..490bbafb52 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactory.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactory.java @@ -17,7 +17,7 @@ package bisq.desktop.main.funds.transactions; -import bisq.core.arbitration.DisputeManager; +import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.offer.OpenOffer; import bisq.core.trade.Tradable; import bisq.core.trade.Trade; @@ -27,18 +27,18 @@ import javax.inject.Singleton; @Singleton public class TransactionAwareTradableFactory { - private final DisputeManager disputeManager; + private final ArbitrationManager arbitrationManager; @Inject - TransactionAwareTradableFactory(DisputeManager disputeManager) { - this.disputeManager = disputeManager; + TransactionAwareTradableFactory(ArbitrationManager arbitrationManager) { + this.arbitrationManager = arbitrationManager; } TransactionAwareTradable create(Tradable delegate) { if (delegate instanceof OpenOffer) { return new TransactionAwareOpenOffer((OpenOffer) delegate); } else if (delegate instanceof Trade) { - return new TransactionAwareTrade((Trade) delegate, disputeManager); + return new TransactionAwareTrade((Trade) delegate, arbitrationManager); } else { return new DummyTransactionAwareTradable(delegate); } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java index b45618d962..07c4bd7d80 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java @@ -17,8 +17,8 @@ package bisq.desktop.main.funds.transactions; -import bisq.core.arbitration.Dispute; -import bisq.core.arbitration.DisputeManager; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.offer.Offer; import bisq.core.trade.Tradable; import bisq.core.trade.Trade; @@ -31,11 +31,11 @@ import java.util.Optional; class TransactionAwareTrade implements TransactionAwareTradable { private final Trade delegate; - private final DisputeManager disputeManager; + private final ArbitrationManager arbitrationManager; - TransactionAwareTrade(Trade delegate, DisputeManager disputeManager) { + TransactionAwareTrade(Trade delegate, ArbitrationManager arbitrationManager) { this.delegate = delegate; - this.disputeManager = disputeManager; + this.arbitrationManager = arbitrationManager; } @Override @@ -75,7 +75,7 @@ class TransactionAwareTrade implements TransactionAwareTradable { private boolean isDisputedPayoutTx(String txId) { String delegateId = delegate.getId(); - ObservableList disputes = disputeManager.getDisputesAsObservableList(); + ObservableList disputes = arbitrationManager.getDisputesAsObservableList(); return disputes.stream() .anyMatch(dispute -> { String disputePayoutTxId = dispute.getDisputePayoutTxId(); diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java index afd36b5861..51a58f24c5 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java @@ -215,7 +215,7 @@ class TransactionsListItem { } else if (trade.getPayoutTx() != null && trade.getPayoutTx().getHashAsString().equals(txId)) { details = Res.get("funds.tx.multiSigPayout", id); - } else if (trade.getDisputeState() != Trade.DisputeState.NO_DISPUTE) { + } else if (trade.getDisputeState() == Trade.DisputeState.DISPUTE_CLOSED) { if (valueSentToMe.isPositive()) { details = Res.get("funds.tx.disputePayout", id); } else { diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java index cb03b9b236..c3e5ff5ae4 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsView.java @@ -541,7 +541,7 @@ public class TransactionsView extends ActivatableView { } private void revertTransaction(String txId, @Nullable Tradable tradable) { - if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { try { btcWalletService.doubleSpendTransaction(txId, () -> { if (tradable != null) @@ -552,8 +552,6 @@ public class TransactionsView extends ActivatableView { } catch (Throwable e) { new Popup<>().warning(e.getMessage()).show(); } - } else { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); } } diff --git a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java index f3565535d0..1ffa45f905 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java @@ -319,7 +319,7 @@ public class WithdrawalView extends ActivatableView { /////////////////////////////////////////////////////////////////////////////////////////// private void onWithdraw() { - if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { try { // We do not know sendersAmount if senderPaysFee is true. We repeat fee calculation after first attempt if senderPaysFee is true. Transaction feeEstimationTransaction = walletService.getFeeEstimationTransactionForMultipleAddresses(fromAddresses, amountAsCoin); @@ -385,8 +385,6 @@ public class WithdrawalView extends ActivatableView { log.error(e.toString()); new Popup<>().warning(e.toString()).show(); } - } else { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/BuyOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/BuyOfferView.java index f722b3252c..9da1283a11 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/BuyOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/BuyOfferView.java @@ -21,8 +21,11 @@ import bisq.desktop.Navigation; import bisq.desktop.common.view.FxmlView; import bisq.desktop.common.view.ViewLoader; -import bisq.core.arbitration.ArbitratorManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.network.p2p.P2PService; import javax.inject.Inject; @@ -30,8 +33,18 @@ import javax.inject.Inject; public class BuyOfferView extends OfferView { @Inject - public BuyOfferView(ViewLoader viewLoader, Navigation navigation, Preferences preferences, ArbitratorManager arbitratorManager) { - super(viewLoader, navigation, preferences, arbitratorManager); + public BuyOfferView(ViewLoader viewLoader, + Navigation navigation, + Preferences preferences, + ArbitratorManager arbitratorManager, + User user, + P2PService p2PService) { + super(viewLoader, + navigation, + preferences, + arbitratorManager, + user, + p2PService); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java index ab9a0f10f1..65cf821109 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java @@ -17,9 +17,11 @@ package bisq.desktop.main.offer; +import bisq.desktop.Navigation; +import bisq.desktop.util.GUIUtil; + import bisq.core.account.witness.AccountAgeRestrictions; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.arbitration.Arbitrator; import bisq.core.btc.TxFeeEstimationService; import bisq.core.btc.listeners.BalanceListener; import bisq.core.btc.listeners.BsqBalanceListener; @@ -107,6 +109,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs private final ReferralIdService referralIdService; private final BSFormatter btcFormatter; private MakerFeeProvider makerFeeProvider; + private final Navigation navigation; private final String offerId; private final BalanceListener btcBalanceListener; private final SetChangeListener paymentAccountsChangeListener; @@ -159,7 +162,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs TxFeeEstimationService txFeeEstimationService, ReferralIdService referralIdService, BSFormatter btcFormatter, - MakerFeeProvider makerFeeProvider) { + MakerFeeProvider makerFeeProvider, + Navigation navigation) { super(btcWalletService); this.openOfferManager = openOfferManager; @@ -176,6 +180,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs this.referralIdService = referralIdService; this.btcFormatter = btcFormatter; this.makerFeeProvider = makerFeeProvider; + this.navigation = navigation; offerId = Utilities.getRandomPrefix(5, 8) + "-" + UUID.randomUUID().toString() + "-" + @@ -556,20 +561,11 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs return paymentAccount; } - boolean hasAcceptedArbitrators() { - final List acceptedArbitrators = user.getAcceptedArbitrators(); - return acceptedArbitrators != null && acceptedArbitrators.size() > 0; - } - protected void setUseMarketBasedPrice(boolean useMarketBasedPrice) { this.useMarketBasedPrice.set(useMarketBasedPrice); preferences.setUsePercentageBasedPrice(useMarketBasedPrice); } - /*boolean isFeeFromFundingTxSufficient() { - return !isMainNet.get() || feeFromFundingTxProperty.get().compareTo(FeePolicy.getMinRequiredFeeForFundingTx()) >= 0; - }*/ - public ObservableList getPaymentAccounts() { return paymentAccounts; } @@ -831,4 +827,9 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs public boolean isHalCashAccount() { return paymentAccount instanceof HalCashAccount; } + + public boolean canPlaceOffer() { + return GUIUtil.isBootstrappedOrShowPopup(p2PService) && + GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java index 75770ae7bd..11af03f826 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java @@ -181,8 +181,13 @@ public abstract class MutableOfferView extends // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// - public MutableOfferView(M model, Navigation navigation, Preferences preferences, Transitions transitions, - OfferDetailsWindow offerDetailsWindow, BSFormatter btcFormatter, BsqFormatter bsqFormatter) { + public MutableOfferView(M model, + Navigation navigation, + Preferences preferences, + Transitions transitions, + OfferDetailsWindow offerDetailsWindow, + BSFormatter btcFormatter, + BsqFormatter bsqFormatter) { super(model); this.navigation = navigation; @@ -354,28 +359,21 @@ public abstract class MutableOfferView extends } private void onPlaceOffer() { - if (model.isReadyForTxBroadcast()) { + if (model.getDataModel().canPlaceOffer()) { if (model.getDataModel().isMakerFeeValid()) { - if (model.hasAcceptedArbitrators()) { - Offer offer = model.createAndGetOffer(); - //noinspection PointlessBooleanExpression - if (!DevEnv.isDevMode()) { - offerDetailsWindow.onPlaceOffer(() -> - model.onPlaceOffer(offer, offerDetailsWindow::hide)) - .show(offer); - } else { - balanceSubscription.unsubscribe(); - model.onPlaceOffer(offer, () -> { - }); - } + Offer offer = model.createAndGetOffer(); + if (!DevEnv.isDevMode()) { + offerDetailsWindow.onPlaceOffer(() -> + model.onPlaceOffer(offer, offerDetailsWindow::hide)) + .show(offer); } else { - new Popup<>().warning(Res.get("popup.warning.noArbitratorsAvailable")).show(); + balanceSubscription.unsubscribe(); + model.onPlaceOffer(offer, () -> { + }); } } else { showInsufficientBsqFundsForBtcFeePaymentPopup(); } - } else { - model.showNotReadyForTxBroadcastPopups(); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java index 917ded3f4f..f397104609 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferViewModel.java @@ -34,7 +34,6 @@ import bisq.desktop.util.validation.FiatVolumeValidator; import bisq.desktop.util.validation.MonetaryValidator; import bisq.desktop.util.validation.SecurityDepositValidator; -import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.Restrictions; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; @@ -54,8 +53,6 @@ import bisq.core.util.BSFormatter; import bisq.core.util.BsqFormatter; import bisq.core.util.validation.InputValidator; -import bisq.network.p2p.P2PService; - import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.app.DevEnv; @@ -86,8 +83,6 @@ public abstract class MutableOfferViewModel ext private final BtcValidator btcValidator; private final BsqValidator bsqValidator; protected final SecurityDepositValidator securityDepositValidator; - private final P2PService p2PService; - private final WalletsSetup walletsSetup; private final PriceFeedService priceFeedService; private final Navigation navigation; private final Preferences preferences; @@ -187,8 +182,6 @@ public abstract class MutableOfferViewModel ext BtcValidator btcValidator, BsqValidator bsqValidator, SecurityDepositValidator securityDepositValidator, - P2PService p2PService, - WalletsSetup walletsSetup, PriceFeedService priceFeedService, Navigation navigation, Preferences preferences, @@ -202,8 +195,6 @@ public abstract class MutableOfferViewModel ext this.btcValidator = btcValidator; this.bsqValidator = bsqValidator; this.securityDepositValidator = securityDepositValidator; - this.p2PService = p2PService; - this.walletsSetup = walletsSetup; this.priceFeedService = priceFeedService; this.navigation = navigation; this.preferences = preferences; @@ -223,8 +214,8 @@ public abstract class MutableOfferViewModel ext public void activate() { if (DevEnv.isDevMode()) { UserThread.runAfter(() -> { - amount.set("1"); - price.set("0.03"); + amount.set("0.001"); + price.set("75000"); // for CNY minAmount.set(amount.get()); onFocusOutPriceAsPercentageTextField(true, false); applyMakerFee(); @@ -1040,22 +1031,11 @@ public abstract class MutableOfferViewModel ext return offer; } - boolean hasAcceptedArbitrators() { - return dataModel.hasAcceptedArbitrators(); - } - - boolean isReadyForTxBroadcast() { - return GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup); - } - - void showNotReadyForTxBroadcastPopups() { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); - } - public M getDataModel() { return dataModel; } + /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/offer/OfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/OfferView.java index b4c6d3603e..8f5db324dc 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/OfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/OfferView.java @@ -26,8 +26,8 @@ import bisq.desktop.main.offer.createoffer.CreateOfferView; import bisq.desktop.main.offer.offerbook.OfferBookView; import bisq.desktop.main.offer.takeoffer.TakeOfferView; import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.GUIUtil; -import bisq.core.arbitration.ArbitratorManager; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.GlobalSettings; import bisq.core.locale.LanguageUtil; @@ -35,9 +35,11 @@ import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.user.Preferences; +import bisq.core.user.User; -import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.P2PService; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; @@ -64,6 +66,8 @@ public abstract class OfferView extends ActivatableView { private final ViewLoader viewLoader; private final Navigation navigation; private final Preferences preferences; + private final User user; + private final P2PService p2PService; private final OfferPayload.Direction direction; private final ArbitratorManager arbitratorManager; @@ -74,10 +78,17 @@ public abstract class OfferView extends ActivatableView { private ChangeListener tabChangeListener; private ListChangeListener tabListChangeListener; - protected OfferView(ViewLoader viewLoader, Navigation navigation, Preferences preferences, ArbitratorManager arbitratorManager) { + protected OfferView(ViewLoader viewLoader, + Navigation navigation, + Preferences preferences, + ArbitratorManager arbitratorManager, + User user, + P2PService p2PService) { this.viewLoader = viewLoader; this.navigation = navigation; this.preferences = preferences; + this.user = user; + this.p2PService = p2PService; this.direction = (this instanceof BuyOfferView) ? OfferPayload.Direction.BUY : OfferPayload.Direction.SELL; this.arbitratorManager = arbitratorManager; } @@ -168,12 +179,9 @@ public abstract class OfferView extends ActivatableView { @Override public void onCreateOffer(TradeCurrency tradeCurrency) { if (!createOfferViewOpen) { - boolean arbitratorAvailableForLanguage = arbitratorManager.isArbitratorAvailableForLanguage(preferences.getUserLanguage()); - if (!arbitratorAvailableForLanguage) { - showNoArbitratorForUserLocaleWarning(); + if (canCreateOrTakeOffer()) { + openCreateOffer(tradeCurrency); } - openCreateOffer(tradeCurrency); - } else { log.error("You have already a \"Create offer\" tab open."); } @@ -182,17 +190,7 @@ public abstract class OfferView extends ActivatableView { @Override public void onTakeOffer(Offer offer) { if (!takeOfferViewOpen) { - List arbitratorNodeAddresses = offer.getArbitratorNodeAddresses(); - List arbitratorLanguages = arbitratorManager.getArbitratorLanguages(arbitratorNodeAddresses); - if (arbitratorLanguages.isEmpty()) { - // In case we get an offer which has been created with arbitrators which are not available - // anymore we don't want to call the showNoArbitratorForUserLocaleWarning - openTakeOffer(offer); - } else { - if (arbitratorLanguages.stream() - .noneMatch(languages -> languages.equals(preferences.getUserLanguage()))) { - showNoArbitratorForUserLocaleWarning(); - } + if (canCreateOrTakeOffer()) { openTakeOffer(offer); } } else { @@ -233,6 +231,11 @@ public abstract class OfferView extends ActivatableView { } } + protected boolean canCreateOrTakeOffer() { + return GUIUtil.isBootstrappedOrShowPopup(p2PService) && + GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation); + } + private void showNoArbitratorForUserLocaleWarning() { String key = "NoArbitratorForUserLocaleWarning"; new Popup<>().information(Res.get("offerbook.info.noArbitrationInUserLanguage", @@ -243,7 +246,7 @@ public abstract class OfferView extends ActivatableView { } private String getArbitrationLanguages() { - return arbitratorManager.getArbitratorsObservableMap().values().stream() + return arbitratorManager.getObservableMap().values().stream() .flatMap(arbitrator -> arbitrator.getLanguageCodes().stream()) .distinct() .map(languageCode -> LanguageUtil.getDisplayName(languageCode)) @@ -253,15 +256,13 @@ public abstract class OfferView extends ActivatableView { private void openTakeOffer(Offer offer) { OfferView.this.takeOfferViewOpen = true; OfferView.this.offer = offer; - OfferView.this.navigation.navigateTo(MainView.class, OfferView.this.getClass(), - TakeOfferView.class); + OfferView.this.navigation.navigateTo(MainView.class, OfferView.this.getClass(), TakeOfferView.class); } private void openCreateOffer(TradeCurrency tradeCurrency) { OfferView.this.createOfferViewOpen = true; OfferView.this.tradeCurrency = tradeCurrency; - OfferView.this.navigation.navigateTo(MainView.class, OfferView.this.getClass(), - CreateOfferView.class); + OfferView.this.navigation.navigateTo(MainView.class, OfferView.this.getClass(), CreateOfferView.class); } private void onCreateOfferViewRemoved() { diff --git a/desktop/src/main/java/bisq/desktop/main/offer/SellOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/SellOfferView.java index 4c79ba9b4e..7c8e57f78b 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/SellOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/SellOfferView.java @@ -21,8 +21,11 @@ import bisq.desktop.Navigation; import bisq.desktop.common.view.FxmlView; import bisq.desktop.common.view.ViewLoader; -import bisq.core.arbitration.ArbitratorManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.network.p2p.P2PService; import javax.inject.Inject; @@ -30,8 +33,18 @@ import javax.inject.Inject; public class SellOfferView extends OfferView { @Inject - public SellOfferView(ViewLoader viewLoader, Navigation navigation, Preferences preferences, ArbitratorManager arbitratorManager) { - super(viewLoader, navigation, preferences, arbitratorManager); + public SellOfferView(ViewLoader viewLoader, + Navigation navigation, + Preferences preferences, + ArbitratorManager arbitratorManager, + User user, + P2PService p2PService) { + super(viewLoader, + navigation, + preferences, + arbitratorManager, + user, + p2PService); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java index bed681ae25..06c2cfb42e 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java @@ -21,6 +21,7 @@ see . package bisq.desktop.main.offer.createoffer; +import bisq.desktop.Navigation; import bisq.desktop.main.offer.MakerFeeProvider; import bisq.desktop.main.offer.MutableOfferDataModel; @@ -65,7 +66,8 @@ class CreateOfferDataModel extends MutableOfferDataModel { TxFeeEstimationService txFeeEstimationService, ReferralIdService referralIdService, BSFormatter btcFormatter, - MakerFeeProvider makerFeeProvider) { + MakerFeeProvider makerFeeProvider, + Navigation navigation) { super(openOfferManager, btcWalletService, bsqWalletService, @@ -80,6 +82,7 @@ class CreateOfferDataModel extends MutableOfferDataModel { txFeeEstimationService, referralIdService, btcFormatter, - makerFeeProvider); + makerFeeProvider, + navigation); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModel.java index 65e0d84dcc..87e33952b1 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModel.java @@ -27,20 +27,39 @@ import bisq.desktop.util.validation.FiatPriceValidator; import bisq.desktop.util.validation.FiatVolumeValidator; import bisq.desktop.util.validation.SecurityDepositValidator; -import bisq.core.btc.setup.WalletsSetup; import bisq.core.provider.price.PriceFeedService; import bisq.core.user.Preferences; import bisq.core.util.BSFormatter; import bisq.core.util.BsqFormatter; -import bisq.network.p2p.P2PService; - import com.google.inject.Inject; class CreateOfferViewModel extends MutableOfferViewModel implements ViewModel { @Inject - public CreateOfferViewModel(CreateOfferDataModel dataModel, FiatVolumeValidator fiatVolumeValidator, FiatPriceValidator fiatPriceValidator, AltcoinValidator altcoinValidator, BtcValidator btcValidator, BsqValidator bsqValidator, SecurityDepositValidator securityDepositValidator, P2PService p2PService, WalletsSetup walletsSetup, PriceFeedService priceFeedService, Navigation navigation, Preferences preferences, BSFormatter btcFormatter, BsqFormatter bsqFormatter) { - super(dataModel, fiatVolumeValidator, fiatPriceValidator, altcoinValidator, btcValidator, bsqValidator, securityDepositValidator, p2PService, walletsSetup, priceFeedService, navigation, preferences, btcFormatter, bsqFormatter); + public CreateOfferViewModel(CreateOfferDataModel dataModel, + FiatVolumeValidator fiatVolumeValidator, + FiatPriceValidator fiatPriceValidator, + AltcoinValidator altcoinValidator, + BtcValidator btcValidator, + BsqValidator bsqValidator, + SecurityDepositValidator securityDepositValidator, + PriceFeedService priceFeedService, + Navigation navigation, + Preferences preferences, + BSFormatter btcFormatter, + BsqFormatter bsqFormatter) { + super(dataModel, + fiatVolumeValidator, + fiatPriceValidator, + altcoinValidator, + btcValidator, + bsqValidator, + securityDepositValidator, + priceFeedService, + navigation, + preferences, + btcFormatter, + bsqFormatter); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java index fc93632c93..4dc6850977 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java @@ -333,7 +333,8 @@ public class OfferBookView extends ActivatableViewAndModel {}); + currencySelectionSubscriber = currencySelectionBinding.subscribe((observable, oldValue, newValue) -> { + }); tableView.setItems(model.getOfferList()); @@ -502,29 +503,25 @@ public class OfferBookView extends ActivatableViewAndModel().headLine(Res.get("offerbook.warning.noTradingAccountForCurrency.headline")) - .instruction(Res.get("offerbook.warning.noTradingAccountForCurrency.msg")) - .actionButtonText(Res.get("offerbook.yesCreateOffer")) - .onAction(() -> { - createOfferButton.setDisable(true); - offerActionHandler.onCreateOffer(model.getSelectedTradeCurrency()); - }) - .secondaryActionButtonText(Res.get("offerbook.setupNewAccount")) - .onSecondaryAction(() -> { - navigation.setReturnPath(navigation.getCurrentPath()); - navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); - }) - .width(725) - .show(); - } else if (!model.hasAcceptedArbitrators()) { - new Popup<>().warning(Res.get("popup.warning.noArbitratorsAvailable")).show(); - } else { + if (model.canCreateOrTakeOffer()) { + if (!model.hasPaymentAccountForCurrency()) { + new Popup<>().headLine(Res.get("offerbook.warning.noTradingAccountForCurrency.headline")) + .instruction(Res.get("offerbook.warning.noTradingAccountForCurrency.msg")) + .actionButtonText(Res.get("offerbook.yesCreateOffer")) + .onAction(() -> { + createOfferButton.setDisable(true); + offerActionHandler.onCreateOffer(model.getSelectedTradeCurrency()); + }) + .secondaryActionButtonText(Res.get("offerbook.setupNewAccount")) + .onSecondaryAction(() -> { + navigation.setReturnPath(navigation.getCurrentPath()); + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + }) + .width(725) + .show(); + return; + } + createOfferButton.setDisable(true); offerActionHandler.onCreateOffer(model.getSelectedTradeCurrency()); } @@ -584,7 +581,7 @@ public class OfferBookView extends ActivatableViewAndModel().confirmation(Res.get("popup.info.cashDepositInfo", offer.getBankId())) @@ -594,25 +591,22 @@ public class OfferBookView extends ActivatableViewAndModel().information(Res.get("popup.warning.notFullyConnected")).show(); } } private void onRemoveOpenOffer(Offer offer) { - if (model.isBootstrapped()) { + if (model.isBootstrappedOrShowPopup()) { String key = "RemoveOfferWarning"; - if (DontShowAgainLookup.showAgain(key)) + if (DontShowAgainLookup.showAgain(key)) { new Popup<>().warning(Res.get("popup.warning.removeOffer", model.formatter.formatCoinWithCode(offer.getMakerFee()))) .actionButtonText(Res.get("shared.removeOffer")) .onAction(() -> doRemoveOffer(offer)) .closeButtonText(Res.get("shared.dontRemoveOffer")) .dontShowAgainId(key) .show(); - else + } else { doRemoveOffer(offer); - } else { - new Popup<>().information(Res.get("popup.warning.notFullyConnected")).show(); + } } } @@ -827,7 +821,9 @@ public class OfferBookView extends ActivatableViewAndModel listener = new ChangeListener<>() { @Override - public void changed(ObservableValue observable, Number oldValue, Number newValue) { + public void changed(ObservableValue observable, + Number oldValue, + Number newValue) { if (offerBookListItem != null && offerBookListItem.getOffer().getVolume() != null) { setText(""); setGraphic(new ColoredDecimalPlacesWithZerosText(model.getVolume(offerBookListItem), diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java index dfc527a600..d1824de6f6 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookViewModel.java @@ -131,7 +131,6 @@ class OfferBookViewModel extends ActivatableViewModel { // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// - @SuppressWarnings("WeakerAccess") @Inject public OfferBookViewModel(User user, OpenOfferManager openOfferManager, @@ -322,8 +321,8 @@ class OfferBookViewModel extends ActivatableViewModel { return allTradeCurrencies; } - boolean isBootstrapped() { - return p2PService.isBootstrapped(); + boolean isBootstrappedOrShowPopup() { + return GUIUtil.isBootstrappedOrShowPopup(p2PService); } TradeCurrency getSelectedTradeCurrency() { @@ -478,6 +477,7 @@ class OfferBookViewModel extends ActivatableViewModel { return PaymentAccountUtil.getMostMaturePaymentAccountForOffer(offer, user.getPaymentAccounts(), accountAgeWitnessService); } + /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// @@ -503,10 +503,6 @@ class OfferBookViewModel extends ActivatableViewModel { // Checks /////////////////////////////////////////////////////////////////////////////////////////// - boolean hasPaymentAccount() { - return user.currentPaymentAccountProperty().get() != null; - } - boolean isAnyPaymentAccountValidForOffer(Offer offer) { return user.getPaymentAccounts() != null && PaymentAccountUtil.isAnyTakerPaymentAccountValidForOffer(offer, user.getPaymentAccounts()); @@ -534,10 +530,12 @@ class OfferBookViewModel extends ActivatableViewModel { PaymentAccountUtil.hasMakerAnyMatureAccountForBuyOffer(user.getPaymentAccounts(), accountAgeWitnessService)); } - boolean hasAcceptedArbitrators() { - return user.getAcceptedArbitrators() != null && !user.getAcceptedArbitrators().isEmpty(); + boolean canCreateOrTakeOffer() { + return GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation) && + GUIUtil.isBootstrappedOrShowPopup(p2PService); } + /////////////////////////////////////////////////////////////////////////////////////////// // Filters /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java index e27356082d..8b30e527b7 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -17,12 +17,13 @@ package bisq.desktop.main.offer.takeoffer; +import bisq.desktop.Navigation; import bisq.desktop.main.offer.OfferDataModel; import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.GUIUtil; import bisq.core.account.witness.AccountAgeRestrictions; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.arbitration.Arbitrator; import bisq.core.btc.TxFeeEstimationService; import bisq.core.btc.listeners.BalanceListener; import bisq.core.btc.model.AddressEntry; @@ -49,6 +50,8 @@ import bisq.core.user.Preferences; import bisq.core.user.User; import bisq.core.util.CoinUtil; +import bisq.network.p2p.P2PService; + import bisq.common.util.Tuple2; import org.bitcoinj.core.Coin; @@ -63,7 +66,6 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; -import java.util.List; import java.util.Set; import org.jetbrains.annotations.NotNull; @@ -88,6 +90,8 @@ class TakeOfferDataModel extends OfferDataModel { private final TxFeeEstimationService txFeeEstimationService; private final PriceFeedService priceFeedService; private final AccountAgeWitnessService accountAgeWitnessService; + private final Navigation navigation; + private final P2PService p2PService; private Coin txFeeFromFeeService; private Coin securityDeposit; @@ -124,7 +128,10 @@ class TakeOfferDataModel extends OfferDataModel { Preferences preferences, TxFeeEstimationService txFeeEstimationService, PriceFeedService priceFeedService, - AccountAgeWitnessService accountAgeWitnessService) { + AccountAgeWitnessService accountAgeWitnessService, + Navigation navigation, + P2PService p2PService + ) { super(btcWalletService); this.tradeManager = tradeManager; @@ -136,8 +143,8 @@ class TakeOfferDataModel extends OfferDataModel { this.txFeeEstimationService = txFeeEstimationService; this.priceFeedService = priceFeedService; this.accountAgeWitnessService = accountAgeWitnessService; - - // isMainNet.set(preferences.getBaseCryptoNetwork() == BitcoinNetwork.BTC_MAINNET); + this.navigation = navigation; + this.p2PService = p2PService; } @Override @@ -158,10 +165,12 @@ class TakeOfferDataModel extends OfferDataModel { if (isTabSelected) priceFeedService.setCurrencyCode(offer.getCurrencyCode()); - tradeManager.checkOfferAvailability(offer, - () -> { - }, - errorMessage -> new Popup<>().warning(errorMessage).show()); + if (canTakeOffer()) { + tradeManager.checkOfferAvailability(offer, + () -> { + }, + errorMessage -> new Popup<>().warning(errorMessage).show()); + } } @Override @@ -171,6 +180,7 @@ class TakeOfferDataModel extends OfferDataModel { tradeManager.onCancelAvailabilityRequest(offer); } + /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @@ -423,11 +433,6 @@ class TakeOfferDataModel extends OfferDataModel { .orElse(firstItem); } - boolean hasAcceptedArbitrators() { - final List acceptedArbitrators = user.getAcceptedArbitrators(); - return acceptedArbitrators != null && acceptedArbitrators.size() > 0; - } - long getMaxTradeLimit() { if (paymentAccount != null) return AccountAgeRestrictions.getMyTradeLimitAtTakeOffer(accountAgeWitnessService, paymentAccount, offer, getCurrencyCode(), getDirection()); @@ -435,6 +440,11 @@ class TakeOfferDataModel extends OfferDataModel { return 0; } + boolean canTakeOffer() { + return GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation) && + GUIUtil.isBootstrappedOrShowPopup(p2PService); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Bindings, listeners diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java index f7a4c75c49..effceff31b 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java @@ -400,10 +400,8 @@ public class TakeOfferView extends ActivatableViewAndModel().information(Res.get("takeOffer.alreadyFunded.movedFunds")) @@ -426,36 +424,28 @@ public class TakeOfferView extends ActivatableViewAndModel - model.onTakeOffer(() -> { - offerDetailsWindow.hide(); - offerDetailsWindowDisplayed = false; - }) - ).show(model.getOffer(), model.dataModel.getAmount().get(), model.dataModel.tradePrice); - offerDetailsWindowDisplayed = true; - } else { - balanceSubscription.unsubscribe(); - model.onTakeOffer(() -> { - }); - } + if (!DevEnv.isDevMode()) { + offerDetailsWindow.onTakeOffer(() -> + model.onTakeOffer(() -> { + offerDetailsWindow.hide(); + offerDetailsWindowDisplayed = false; + }) + ).show(model.getOffer(), model.dataModel.getAmount().get(), model.dataModel.tradePrice); + offerDetailsWindowDisplayed = true; } else { - new Popup<>().warning(Res.get("popup.warning.noArbitratorsAvailable")).show(); + balanceSubscription.unsubscribe(); + model.onTakeOffer(() -> { + }); } } else { showInsufficientBsqFundsForBtcFeePaymentPopup(); } - } else { - model.showNotReadyForTxBroadcastPopups(); } } - @SuppressWarnings("PointlessBooleanExpression") private void onShowPayFundsScreen() { scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java index 55dcbe0b2e..526b1b22e9 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java @@ -754,18 +754,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel im return dataModel.getLastSelectedPaymentAccount(); } - boolean hasAcceptedArbitrators() { - return dataModel.hasAcceptedArbitrators(); - } - - boolean isReadyForTxBroadcast() { - return GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup); - } - - void showNotReadyForTxBroadcastPopups() { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); - } - public void resetOfferWarning() { offerWarning.set(null); } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/Notification.java b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/Notification.java index b26eedd385..a076981d05 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/Notification.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/Notification.java @@ -24,6 +24,7 @@ import bisq.core.locale.Res; import bisq.common.Timer; import bisq.common.UserThread; +import bisq.common.app.DevEnv; import de.jensd.fx.fontawesome.AwesomeIcon; @@ -91,6 +92,9 @@ public class Notification extends Overlay { @Override public void show() { + if (DevEnv.isDevMode()) { + return; + } super.show(); hasBeenDisplayed = true; } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java index 04e9bfdf46..15372e9820 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java @@ -19,13 +19,16 @@ package bisq.desktop.main.overlays.notifications; import bisq.desktop.Navigation; import bisq.desktop.main.MainView; -import bisq.desktop.main.disputes.DisputesView; -import bisq.desktop.main.disputes.trader.TraderDisputeView; import bisq.desktop.main.portfolio.PortfolioView; import bisq.desktop.main.portfolio.pendingtrades.PendingTradesView; +import bisq.desktop.main.support.SupportView; +import bisq.desktop.main.support.dispute.client.DisputeClientView; +import bisq.desktop.main.support.dispute.client.arbitration.ArbitrationClientView; +import bisq.desktop.main.support.dispute.client.mediation.MediationClientView; -import bisq.core.arbitration.DisputeManager; import bisq.core.locale.Res; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.trade.BuyerTrade; import bisq.core.trade.MakerTrade; import bisq.core.trade.SellerTrade; @@ -35,6 +38,7 @@ import bisq.core.user.DontShowAgainLookup; import bisq.core.user.Preferences; import bisq.common.UserThread; +import bisq.common.app.DevEnv; import com.google.inject.Inject; @@ -78,7 +82,8 @@ public class NotificationCenter { /////////////////////////////////////////////////////////////////////////////////////////// private final TradeManager tradeManager; - private final DisputeManager disputeManager; + private final ArbitrationManager arbitrationManager; + private final MediationManager mediationManager; private final Navigation navigation; private final Map disputeStateSubscriptionsMap = new HashMap<>(); @@ -91,9 +96,14 @@ public class NotificationCenter { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public NotificationCenter(TradeManager tradeManager, DisputeManager disputeManager, Preferences preferences, Navigation navigation) { + public NotificationCenter(TradeManager tradeManager, + ArbitrationManager arbitrationManager, + MediationManager mediationManager, + Preferences preferences, + Navigation navigation) { this.tradeManager = tradeManager; - this.disputeManager = disputeManager; + this.arbitrationManager = arbitrationManager; + this.mediationManager = mediationManager; this.navigation = navigation; EasyBind.subscribe(preferences.getUseAnimationsProperty(), useAnimations -> NotificationCenter.useAnimations = useAnimations); @@ -220,8 +230,8 @@ public class NotificationCenter { private void onDisputeStateChanged(Trade trade, Trade.DisputeState disputeState) { String message = null; - if (disputeManager.findOwnDispute(trade.getId()).isPresent()) { - String disputeOrTicket = disputeManager.findOwnDispute(trade.getId()).get().isSupportTicket() ? + if (arbitrationManager.findOwnDispute(trade.getId()).isPresent()) { + String disputeOrTicket = arbitrationManager.findOwnDispute(trade.getId()).get().isSupportTicket() ? Res.get("shared.supportTicket") : Res.get("shared.dispute"); switch (disputeState) { @@ -235,18 +245,56 @@ public class NotificationCenter { case DISPUTE_CLOSED: message = Res.get("notification.trade.disputeClosed", disputeOrTicket); break; + default: + if (DevEnv.isDevMode()) { + log.error("arbitrationDisputeManager must not contain mediation disputes"); + throw new RuntimeException("arbitrationDisputeManager must not contain mediation disputes"); + } + break; } if (message != null) { - Notification notification = new Notification().disputeHeadLine(trade.getShortId()).message(message); - if (navigation.getCurrentPath() != null && !navigation.getCurrentPath().contains(TraderDisputeView.class)) { - notification.actionButtonTextWithGoTo("navigation.support") - .onAction(() -> navigation.navigateTo(MainView.class, DisputesView.class, TraderDisputeView.class)) - .show(); - } else { - notification.show(); - } + goToSupport(trade, message, false); + } + } + if (mediationManager.findOwnDispute(trade.getId()).isPresent()) { + String disputeOrTicket = mediationManager.findOwnDispute(trade.getId()).get().isSupportTicket() ? + Res.get("shared.supportTicket") : + Res.get("shared.dispute"); + switch (disputeState) { + // TODO + case MEDIATION_REQUESTED: + break; + case MEDIATION_STARTED_BY_PEER: + message = Res.get("notification.trade.peerOpenedDispute", disputeOrTicket); + break; + case MEDIATION_CLOSED: + message = Res.get("notification.trade.disputeClosed", disputeOrTicket); + break; + default: + if (DevEnv.isDevMode()) { + log.error("mediationDisputeManager must not contain arbitration disputes"); + throw new RuntimeException("mediationDisputeManager must not contain arbitration disputes"); + } + break; + } + if (message != null) { + goToSupport(trade, message, true); } } } + private void goToSupport(Trade trade, String message, boolean isMediation) { + Notification notification = new Notification().disputeHeadLine(trade.getShortId()).message(message); + Class viewClass = isMediation ? + MediationClientView.class : + ArbitrationClientView.class; + if (navigation.getCurrentPath() != null && !navigation.getCurrentPath().contains(viewClass)) { + notification.actionButtonTextWithGoTo("navigation.support") + .onAction(() -> navigation.navigateTo(MainView.class, SupportView.class, viewClass)) + .show(); + } else { + notification.show(); + } + } + } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ContractWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ContractWindow.java index 0d04a44896..bfec422dd6 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ContractWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ContractWindow.java @@ -23,14 +23,17 @@ import bisq.desktop.main.overlays.Overlay; import bisq.desktop.util.Layout; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.arbitration.Dispute; -import bisq.core.arbitration.DisputeManager; import bisq.core.locale.CountryUtil; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.trade.Contract; import bisq.core.util.BSFormatter; @@ -66,7 +69,8 @@ import static bisq.desktop.util.FormBuilder.*; @Slf4j public class ContractWindow extends Overlay { - private final DisputeManager disputeManager; + private final ArbitrationManager arbitrationManager; + private final MediationManager mediationManager; private final AccountAgeWitnessService accountAgeWitnessService; private final BSFormatter formatter; private Dispute dispute; @@ -77,9 +81,12 @@ public class ContractWindow extends Overlay { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public ContractWindow(DisputeManager disputeManager, AccountAgeWitnessService accountAgeWitnessService, + public ContractWindow(ArbitrationManager arbitrationManager, + MediationManager mediationManager, + AccountAgeWitnessService accountAgeWitnessService, BSFormatter formatter) { - this.disputeManager = disputeManager; + this.arbitrationManager = arbitrationManager; + this.mediationManager = mediationManager; this.accountAgeWitnessService = accountAgeWitnessService; this.formatter = formatter; type = Type.Confirmation; @@ -161,15 +168,20 @@ public class ContractWindow extends Overlay { getAccountAge(contract.getBuyerPaymentAccountPayload(), contract.getBuyerPubKeyRing(), offer.getCurrencyCode()) + " / " + getAccountAge(contract.getSellerPaymentAccountPayload(), contract.getSellerPubKeyRing(), offer.getCurrencyCode())); + String nrOfDisputesAsBuyer = getDisputeManager(dispute).getNrOfDisputes(true, contract); + String nrOfDisputesAsSeller = getDisputeManager(dispute).getNrOfDisputes(false, contract); addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("contractWindow.numDisputes"), - disputeManager.getNrOfDisputes(true, contract) + " / " + disputeManager.getNrOfDisputes(false, contract)); + nrOfDisputesAsBuyer + " / " + nrOfDisputesAsSeller); addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.buyer")), contract.getBuyerPaymentAccountPayload().getPaymentDetails()).second.setMouseTransparent(false); addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("shared.paymentDetails", Res.get("shared.seller")), sellerPaymentAccountPayload.getPaymentDetails()).second.setMouseTransparent(false); - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("shared.arbitrator"), contract.getArbitratorNodeAddress().getFullAddress()); + // TODO update in next release to shared.selectedArbitrator and delete shared.arbitrator entry + String title = dispute.isMediationDispute() ? Res.get("shared.selectedMediator") : Res.get("shared.arbitrator"); + String agentNodeAddress = getDisputeManager(dispute).getAgentNodeAddress(dispute).getFullAddress(); + addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, title, agentNodeAddress); if (showAcceptedCountryCodes) { String countries; @@ -258,7 +270,13 @@ public class ContractWindow extends Overlay { }); } - private String getAccountAge(PaymentAccountPayload paymentAccountPayload, PubKeyRing pubKeyRing, String currencyCode) { + private DisputeManager> getDisputeManager(Dispute dispute) { + return dispute.isMediationDispute() ? mediationManager : arbitrationManager; + } + + private String getAccountAge(PaymentAccountPayload paymentAccountPayload, + PubKeyRing pubKeyRing, + String currencyCode) { long age = accountAgeWitnessService.getAccountAge(paymentAccountPayload, pubKeyRing); return CurrencyUtil.isFiatCurrency(currencyCode) ? age > -1 ? Res.get("peerInfoIcon.tooltip.age", formatter.formatAccountAge(age)) : diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index 4cd1d6b55a..ea55d87e6a 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -25,19 +25,23 @@ import bisq.desktop.main.overlays.Overlay; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.Layout; -import bisq.core.arbitration.Dispute; -import bisq.core.arbitration.DisputeManager; -import bisq.core.arbitration.DisputeResult; import bisq.core.btc.exceptions.TransactionVerificationException; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.locale.Res; import bisq.core.offer.Offer; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.trade.Contract; import bisq.core.util.BSFormatter; import bisq.common.UserThread; +import bisq.common.app.DevEnv; import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; @@ -55,14 +59,12 @@ import javafx.scene.control.TextArea; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; import javafx.scene.input.KeyCode; -import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.geometry.HPos; import javafx.geometry.Insets; -import javafx.geometry.VPos; import javafx.beans.binding.Bindings; import javafx.beans.value.ChangeListener; @@ -74,13 +76,17 @@ import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static bisq.desktop.util.FormBuilder.*; +import static bisq.desktop.util.FormBuilder.add2ButtonsWithBox; +import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox; public class DisputeSummaryWindow extends Overlay { private static final Logger log = LoggerFactory.getLogger(DisputeSummaryWindow.class); private final BSFormatter formatter; - private final DisputeManager disputeManager; + private final ArbitrationManager arbitrationManager; + private final MediationManager mediationManager; private final BtcWalletService walletService; private final TradeWalletService tradeWalletService; private Dispute dispute; @@ -92,6 +98,7 @@ public class DisputeSummaryWindow extends Overlay { private RadioButton reasonWasBugRadioButton, reasonWasUsabilityIssueRadioButton, reasonProtocolViolationRadioButton, reasonNoReplyRadioButton, reasonWasScamRadioButton, reasonWasOtherRadioButton, reasonWasBankRadioButton; + // Dispute object of other trade peer. The dispute field is the one from which we opened the close dispute window. private Optional peersDisputeOptional; private String role; private TextArea summaryNotesTextArea; @@ -109,11 +116,15 @@ public class DisputeSummaryWindow extends Overlay { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public DisputeSummaryWindow(BSFormatter formatter, DisputeManager disputeManager, BtcWalletService walletService, + public DisputeSummaryWindow(BSFormatter formatter, + ArbitrationManager arbitrationManager, + MediationManager mediationManager, + BtcWalletService walletService, TradeWalletService tradeWalletService) { this.formatter = formatter; - this.disputeManager = disputeManager; + this.arbitrationManager = arbitrationManager; + this.mediationManager = mediationManager; this.walletService = walletService; this.tradeWalletService = tradeWalletService; @@ -128,6 +139,12 @@ public class DisputeSummaryWindow extends Overlay { createGridPane(); addContent(); display(); + + if (DevEnv.isDevMode()) { + UserThread.execute(() -> { + summaryNotesTextArea.setText("dummy result...."); + }); + } } public DisputeSummaryWindow onFinalizeDispute(Runnable finalizeDisputeHandler) { @@ -182,14 +199,12 @@ public class DisputeSummaryWindow extends Overlay { else disputeResult = dispute.getDisputeResultProperty().get(); - peersDisputeOptional = disputeManager.getDisputesAsObservableList().stream() - .filter(d -> dispute.getTradeId().equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()).findFirst(); + peersDisputeOptional = getDisputeManager(dispute).getDisputesAsObservableList().stream() + .filter(d -> dispute.getTradeId().equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()) + .findFirst(); addInfoPane(); - if (!dispute.isSupportTicket()) - addCheckboxes(); - addTradeAmountPayoutControls(); addPayoutAmountTextFields(); addReasonControls(); @@ -205,11 +220,6 @@ public class DisputeSummaryWindow extends Overlay { disputeResult.setReason(peersDisputeResult.getReason()); disputeResult.setSummaryNotes(peersDisputeResult.summaryNotesProperty().get()); - /* if (disputeResult.getBuyerPayoutAmount() != null) { - log.debug("buyerPayoutAmount " + disputeResult.getBuyerPayoutAmount().toFriendlyString()); - log.debug("sellerPayoutAmount " + disputeResult.getSellerPayoutAmount().toFriendlyString()); - }*/ - buyerGetsTradeAmountRadioButton.setDisable(true); buyerGetsAllRadioButton.setDisable(true); sellerGetsTradeAmountRadioButton.setDisable(true); @@ -278,27 +288,6 @@ public class DisputeSummaryWindow extends Overlay { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("shared.securityDeposit"), securityDeposit); } - private void addCheckboxes() { - Label evidenceLabel = addLabel(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.evidence"), 10); - GridPane.setValignment(evidenceLabel, VPos.TOP); - CheckBox tamperProofCheckBox = new AutoTooltipCheckBox(Res.get("disputeSummaryWindow.evidence.tamperProof")); - CheckBox idVerificationCheckBox = new AutoTooltipCheckBox(Res.get("disputeSummaryWindow.evidence.id")); - CheckBox screenCastCheckBox = new AutoTooltipCheckBox(Res.get("disputeSummaryWindow.evidence.video")); - - tamperProofCheckBox.selectedProperty().bindBidirectional(disputeResult.tamperProofEvidenceProperty()); - idVerificationCheckBox.selectedProperty().bindBidirectional(disputeResult.idVerificationProperty()); - screenCastCheckBox.selectedProperty().bindBidirectional(disputeResult.screenCastProperty()); - - FlowPane checkBoxPane = new FlowPane(); - checkBoxPane.setHgap(20); - checkBoxPane.setVgap(5); - checkBoxPane.getChildren().addAll(tamperProofCheckBox, idVerificationCheckBox, screenCastCheckBox); - GridPane.setRowIndex(checkBoxPane, rowIndex); - GridPane.setColumnIndex(checkBoxPane, 1); - GridPane.setMargin(checkBoxPane, new Insets(10, 0, 0, 0)); - gridPane.getChildren().add(checkBoxPane); - } - private void addTradeAmountPayoutControls() { buyerGetsTradeAmountRadioButton = new AutoTooltipRadioButton(Res.get("disputeSummaryWindow.payout.getsTradeAmount", Res.get("shared.buyer"))); @@ -316,8 +305,7 @@ public class DisputeSummaryWindow extends Overlay { sellerGetsTradeAmountRadioButton, sellerGetsAllRadioButton, customRadioButton); - addTopLabelWithVBox(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.payout"), - radioButtonPane, 10); + addTopLabelWithVBox(gridPane, ++rowIndex, Res.get("disputeSummaryWindow.payout"), radioButtonPane, 0); tradeAmountToggleGroup = new ToggleGroup(); buyerGetsTradeAmountRadioButton.setToggleGroup(tradeAmountToggleGroup); @@ -370,34 +358,35 @@ public class DisputeSummaryWindow extends Overlay { private void applyCustomAmounts(InputTextField inputTextField) { Contract contract = dispute.getContract(); - Coin buyerAmount = formatter.parseToCoin(buyerPayoutAmountInputTextField.getText()); - Coin sellerAmount = formatter.parseToCoin(sellerPayoutAmountInputTextField.getText()); Offer offer = new Offer(contract.getOfferPayload()); - Coin available = contract.getTradeAmount(). - add(offer.getBuyerSecurityDeposit()) + Coin available = contract.getTradeAmount() + .add(offer.getBuyerSecurityDeposit()) .add(offer.getSellerSecurityDeposit()); - Coin totalAmount = buyerAmount.add(sellerAmount); - - if (totalAmount.compareTo(available) > 0) { - new Popup<>().warning(Res.get("disputeSummaryWindow.payout.adjustAmount", available.toFriendlyString())) - .show(); - - if (inputTextField == buyerPayoutAmountInputTextField) { - buyerAmount = available.subtract(sellerAmount); - inputTextField.setText(formatter.formatCoin(buyerAmount)); - } else if (inputTextField == sellerPayoutAmountInputTextField) { - sellerAmount = available.subtract(buyerAmount); - inputTextField.setText(formatter.formatCoin(sellerAmount)); - } + Coin enteredAmount = formatter.parseToCoin(inputTextField.getText()); + if (enteredAmount.compareTo(available) > 0) { + enteredAmount = available; + Coin finalEnteredAmount = enteredAmount; + inputTextField.setText(formatter.formatCoin(finalEnteredAmount)); + } + Coin counterPartAsCoin = available.subtract(enteredAmount); + String formattedCounterPartAmount = formatter.formatCoin(counterPartAsCoin); + Coin buyerAmount; + Coin sellerAmount; + if (inputTextField == buyerPayoutAmountInputTextField) { + buyerAmount = enteredAmount; + sellerAmount = counterPartAsCoin; + sellerPayoutAmountInputTextField.setText(formattedCounterPartAmount); + } else { + sellerAmount = enteredAmount; + buyerAmount = counterPartAsCoin; + buyerPayoutAmountInputTextField.setText(formattedCounterPartAmount); } disputeResult.setBuyerPayoutAmount(buyerAmount); disputeResult.setSellerPayoutAmount(sellerAmount); - - if (buyerAmount.compareTo(sellerAmount) > 0) - disputeResult.setWinner(DisputeResult.Winner.BUYER); - else - disputeResult.setWinner(DisputeResult.Winner.SELLER); + disputeResult.setWinner(buyerAmount.compareTo(sellerAmount) > 0 ? + DisputeResult.Winner.BUYER : + DisputeResult.Winner.SELLER); } private void addPayoutAmountTextFields() { @@ -529,24 +518,16 @@ public class DisputeSummaryWindow extends Overlay { Button cancelButton = tuple.second; - final Dispute finalPeersDispute = peersDisputeOptional.get(); closeTicketButton.setOnAction(e -> { - if (dispute.getDepositTxSerialized() != null) { + if (dispute.getDepositTxSerialized() == null) { + log.warn("dispute.getDepositTxSerialized is null"); + return; + } + + if (!dispute.isMediationDispute()) { try { AddressEntry arbitratorAddressEntry = walletService.getArbitratorAddressEntry(); - disputeResult.setArbitratorPubKey(walletService.getArbitratorAddressEntry().getPubKey()); - - /* byte[] depositTxSerialized, - Coin buyerPayoutAmount, - Coin sellerPayoutAmount, - Coin arbitratorPayoutAmount, - String buyerAddressString, - String sellerAddressString, - AddressEntry arbitratorAddressEntry, - byte[] buyerPubKey, - byte[] sellerPubKey, - byte[] arbitratorPubKey) - */ + disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey()); byte[] arbitratorSignature = tradeWalletService.arbitratorSignsDisputedPayoutTx( dispute.getDepositTxSerialized(), disputeResult.getBuyerPayoutAmount(), @@ -559,41 +540,36 @@ public class DisputeSummaryWindow extends Overlay { arbitratorAddressEntry.getPubKey() ); disputeResult.setArbitratorSignature(arbitratorSignature); - - closeTicketButton.disableProperty().unbind(); - dispute.setDisputeResult(disputeResult); - - disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected()); - disputeResult.setCloseDate(new Date()); - String text = Res.get("disputeSummaryWindow.close.msg", - formatter.formatDateTime(disputeResult.getCloseDate()), - role, - formatter.booleanToYesNo(disputeResult.tamperProofEvidenceProperty().get()), - role, - formatter.booleanToYesNo(disputeResult.idVerificationProperty().get()), - role, - formatter.booleanToYesNo(disputeResult.screenCastProperty().get()), - formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()), - formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()), - disputeResult.summaryNotesProperty().get()); - - dispute.setIsClosed(true); - disputeManager.sendDisputeResultMessage(disputeResult, dispute, text); - - if (!finalPeersDispute.isClosed()) - UserThread.runAfter(() -> - new Popup<>().attention(Res.get("disputeSummaryWindow.close.closePeer")).show(), - 200, TimeUnit.MILLISECONDS); - - hide(); - - finalizeDisputeHandlerOptional.ifPresent(Runnable::run); } catch (AddressFormatException | TransactionVerificationException e2) { - e2.printStackTrace(); + log.error("Error at close dispute", e2); + return; } - } else { - log.warn("dispute.getDepositTxSerialized is null"); } + + disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected()); + disputeResult.setCloseDate(new Date()); + dispute.setDisputeResult(disputeResult); + dispute.setIsClosed(true); + String text = Res.get("disputeSummaryWindow.close.msg", + formatter.formatDateTime(disputeResult.getCloseDate()), + formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()), + formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()), + disputeResult.summaryNotesProperty().get()); + + getDisputeManager(dispute).sendDisputeResultMessage(disputeResult, dispute, text); + + if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { + UserThread.runAfter(() -> new Popup<>() + .attention(Res.get("disputeSummaryWindow.close.closePeer")) + .show(), + 200, TimeUnit.MILLISECONDS); + } + + finalizeDisputeHandlerOptional.ifPresent(Runnable::run); + + closeTicketButton.disableProperty().unbind(); + + hide(); }); cancelButton.setOnAction(e -> { @@ -602,6 +578,10 @@ public class DisputeSummaryWindow extends Overlay { }); } + private DisputeManager> getDisputeManager(Dispute dispute) { + return dispute.isMediationDispute() ? mediationManager : arbitrationManager; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Controller diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/EmptyWalletWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/EmptyWalletWindow.java index c6d0af5bbb..239e64b0a6 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/EmptyWalletWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/EmptyWalletWindow.java @@ -205,7 +205,7 @@ public class EmptyWalletWindow extends Overlay { } private void doEmptyWallet(KeyParameter aesKey) { - if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { if (!openOfferManager.getObservableList().isEmpty()) { UserThread.runAfter(() -> new Popup<>().warning(Res.get("emptyWalletWindow.openOffers.warn")) @@ -215,8 +215,6 @@ public class EmptyWalletWindow extends Overlay { } else { doEmptyWallet2(aesKey); } - } else { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); } } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java index 7d7d7627d2..e3abcbc2c2 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/FilterWindow.java @@ -136,6 +136,7 @@ public class FilterWindow extends Overlay { InputTextField bannedPaymentMethodsInputTextField = addTopLabelInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.bannedPaymentMethods")).second; bannedPaymentMethodsInputTextField.setPromptText("E.g. PERFECT_MONEY"); // Do not translate InputTextField arbitratorsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.arbitrators")); + InputTextField mediatorsInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.mediators")); InputTextField seedNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.seedNode")); InputTextField priceRelayNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.priceRelayNode")); InputTextField btcNodesInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("filterWindow.btcNode")); @@ -172,6 +173,9 @@ public class FilterWindow extends Overlay { if (filter.getArbitrators() != null) arbitratorsInputTextField.setText(filter.getArbitrators().stream().collect(Collectors.joining(", "))); + if (filter.getMediators() != null) + mediatorsInputTextField.setText(filter.getMediators().stream().collect(Collectors.joining(", "))); + if (filter.getSeedNodes() != null) seedNodesInputTextField.setText(filter.getSeedNodes().stream().collect(Collectors.joining(", "))); @@ -195,6 +199,7 @@ public class FilterWindow extends Overlay { List bannedCurrencies = new ArrayList<>(); List bannedPaymentMethods = new ArrayList<>(); List arbitrators = new ArrayList<>(); + List mediators = new ArrayList<>(); List seedNodes = new ArrayList<>(); List priceRelayNodes = new ArrayList<>(); List btcNodes = new ArrayList<>(); @@ -236,6 +241,10 @@ public class FilterWindow extends Overlay { arbitrators = new ArrayList<>(Arrays.asList(StringUtils.deleteWhitespace(arbitratorsInputTextField.getText()).split(","))); } + if (!mediatorsInputTextField.getText().isEmpty()) { + mediators = new ArrayList<>(Arrays.asList(StringUtils.deleteWhitespace(mediatorsInputTextField.getText()).split(","))); + } + if (!seedNodesInputTextField.getText().isEmpty()) { seedNodes = new ArrayList<>(Arrays.asList(StringUtils.deleteWhitespace(seedNodesInputTextField.getText()).split(","))); } @@ -261,7 +270,8 @@ public class FilterWindow extends Overlay { btcNodes, disableDaoCheckBox.isSelected(), disableDaoBelowVersionInputTextField.getText(), - disableTradeBelowVersionInputTextField.getText()), + disableTradeBelowVersionInputTextField.getText(), + mediators), keyInputTextField.getText())) hide(); else diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java index b6ac7e84ed..64b755d5a0 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ManualPayoutTxWindow.java @@ -171,7 +171,7 @@ public class ManualPayoutTxWindow extends Overlay { } }; onAction(() -> { - if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + if (GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup)) { try { tradeWalletService.emergencySignAndPublishPayoutTx(depositTxHex.getText(), Coin.parseCoin(buyerPayoutAmount.getText()), @@ -194,8 +194,6 @@ public class ManualPayoutTxWindow extends Overlay { e.printStackTrace(); UserThread.execute(() -> new Popup<>().warning(e.toString()).show()); } - } else { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); } }); } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/OfferDetailsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/OfferDetailsWindow.java index 6111bd47de..1b3dc847df 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/OfferDetailsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/OfferDetailsWindow.java @@ -21,7 +21,7 @@ import bisq.desktop.Navigation; import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.BusyAnimation; import bisq.desktop.main.overlays.Overlay; -import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.GUIUtil; import bisq.desktop.util.Layout; import bisq.core.locale.BankUtil; @@ -70,6 +70,7 @@ public class OfferDetailsWindow extends Overlay { private final BSFormatter formatter; private final User user; private final KeyRing keyRing; + private final Navigation navigation; private Offer offer; private Coin tradeAmount; private Price tradePrice; @@ -88,6 +89,7 @@ public class OfferDetailsWindow extends Overlay { this.formatter = formatter; this.user = user; this.keyRing = keyRing; + this.navigation = navigation; type = Type.Confirmation; } @@ -293,7 +295,7 @@ public class OfferDetailsWindow extends Overlay { textArea.setEditable(false); } - rows = 4; + rows = 3; if (countryCode != null) rows++; if (offer.getOfferFeePaymentTxId() != null) @@ -321,8 +323,6 @@ public class OfferDetailsWindow extends Overlay { addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("offerDetailsWindow.countryBank"), CountryUtil.getNameAndCode(countryCode)); - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("offerDetailsWindow.acceptedArbitrators"), - formatter.arbitratorAddressesToString(offer.getArbitratorNodeAddresses())); if (offer.getOfferFeePaymentTxId() != null) addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.makerFeeTxId"), offer.getOfferFeePaymentTxId()); @@ -391,20 +391,17 @@ public class OfferDetailsWindow extends Overlay { placeOfferTuple.forth.getChildren().add(cancelButton); button.setOnAction(e -> { - if (user.getAcceptedArbitrators() != null && - user.getAcceptedArbitrators().size() > 0) { + if (GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation)) { button.setDisable(true); cancelButton.setDisable(true); busyAnimation.play(); if (isPlaceOffer) { spinnerInfoLabel.setText(Res.get("createOffer.fundsBox.placeOfferSpinnerInfo")); - placeOfferHandlerOptional.get().run(); + placeOfferHandlerOptional.ifPresent(Runnable::run); } else { spinnerInfoLabel.setText(Res.get("takeOffer.fundsBox.takeOfferSpinnerInfo")); - takeOfferHandlerOptional.get().run(); + takeOfferHandlerOptional.ifPresent(Runnable::run); } - } else { - new Popup<>().warning(Res.get("popup.warning.noArbitratorsAvailable")).show(); } }); } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java index 6c3d94152d..df90c34996 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java @@ -24,16 +24,18 @@ import bisq.desktop.main.overlays.Overlay; import bisq.desktop.util.Layout; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.arbitration.DisputeManager; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.util.BSFormatter; +import bisq.network.p2p.NodeAddress; + import bisq.common.UserThread; import org.bitcoinj.core.Utils; @@ -67,7 +69,7 @@ public class TradeDetailsWindow extends Overlay { protected static final Logger log = LoggerFactory.getLogger(TradeDetailsWindow.class); private final BSFormatter formatter; - private final DisputeManager disputeManager; + private final ArbitrationManager arbitrationManager; private final TradeManager tradeManager; private final AccountAgeWitnessService accountAgeWitnessService; private Trade trade; @@ -82,10 +84,12 @@ public class TradeDetailsWindow extends Overlay { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public TradeDetailsWindow(BSFormatter formatter, DisputeManager disputeManager, TradeManager tradeManager, + public TradeDetailsWindow(BSFormatter formatter, + ArbitrationManager arbitrationManager, + TradeManager tradeManager, AccountAgeWitnessService accountAgeWitnessService) { this.formatter = formatter; - this.disputeManager = disputeManager; + this.arbitrationManager = arbitrationManager; this.tradeManager = tradeManager; this.accountAgeWitnessService = accountAgeWitnessService; type = Type.Confirmation; @@ -185,8 +189,8 @@ public class TradeDetailsWindow extends Overlay { rows++; if (trade.getPayoutTx() != null) rows++; - boolean showDisputedTx = disputeManager.findOwnDispute(trade.getId()).isPresent() && - disputeManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId() != null; + boolean showDisputedTx = arbitrationManager.findOwnDispute(trade.getId()).isPresent() && + arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId() != null; if (showDisputedTx) rows++; if (trade.hasFailed()) @@ -213,9 +217,13 @@ public class TradeDetailsWindow extends Overlay { Res.get("shared.takerTxFee", formatter.formatCoinWithCode(offer.getTxFee().multiply(3L))); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.txFee"), txFee); - if (trade.getArbitratorNodeAddress() != null) - addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("shared.arbitrator"), - trade.getArbitratorNodeAddress().getFullAddress()); + NodeAddress arbitratorNodeAddress = trade.getArbitratorNodeAddress(); + NodeAddress mediatorNodeAddress = trade.getMediatorNodeAddress(); + if (arbitratorNodeAddress != null && mediatorNodeAddress != null) { + addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, + Res.get("tradeDetailsWindow.agentAddresses"), + arbitratorNodeAddress.getFullAddress() + " / " + mediatorNodeAddress.getFullAddress()); + } if (trade.getTradingPeerNodeAddress() != null) addConfirmationLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.tradingPeersOnion"), @@ -266,7 +274,7 @@ public class TradeDetailsWindow extends Overlay { trade.getPayoutTx().getHashAsString()); if (showDisputedTx) addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.disputedPayoutTxId"), - disputeManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId()); + arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId()); if (contract != null) { Button viewContractButton = addConfirmationLabelButton(gridPane, ++rowIndex, Res.get("shared.contractAsJson"), diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/UnlockArbitrationRegistrationWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/UnlockDisputeAgentRegistrationWindow.java similarity index 94% rename from desktop/src/main/java/bisq/desktop/main/overlays/windows/UnlockArbitrationRegistrationWindow.java rename to desktop/src/main/java/bisq/desktop/main/overlays/windows/UnlockDisputeAgentRegistrationWindow.java index e4688f2d3e..cb79bd7afb 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/UnlockArbitrationRegistrationWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/UnlockDisputeAgentRegistrationWindow.java @@ -39,7 +39,7 @@ import javafx.beans.value.ChangeListener; import static bisq.desktop.util.FormBuilder.add2ButtonsAfterGroup; import static bisq.desktop.util.FormBuilder.addTopLabelInputTextFieldWithVBox; -public class UnlockArbitrationRegistrationWindow extends Overlay { +public class UnlockDisputeAgentRegistrationWindow extends Overlay { private final boolean useDevPrivilegeKeys; private Button unlockButton; private InputTextField keyInputTextField; @@ -60,7 +60,7 @@ public class UnlockArbitrationRegistrationWindow extends Overlay { @Inject - public EditOfferViewModel(EditOfferDataModel dataModel, FiatVolumeValidator fiatVolumeValidator, FiatPriceValidator fiatPriceValidator, AltcoinValidator altcoinValidator, BtcValidator btcValidator, BsqValidator bsqValidator, SecurityDepositValidator securityDepositValidator, P2PService p2PService, WalletsSetup walletsSetup, PriceFeedService priceFeedService, Navigation navigation, Preferences preferences, BSFormatter btcFormatter, BsqFormatter bsqFormatter) { - super(dataModel, fiatVolumeValidator, fiatPriceValidator, altcoinValidator, btcValidator, bsqValidator, securityDepositValidator, p2PService, walletsSetup, priceFeedService, navigation, preferences, btcFormatter, bsqFormatter); + public EditOfferViewModel(EditOfferDataModel dataModel, + FiatVolumeValidator fiatVolumeValidator, + FiatPriceValidator fiatPriceValidator, + AltcoinValidator altcoinValidator, + BtcValidator btcValidator, + BsqValidator bsqValidator, + SecurityDepositValidator securityDepositValidator, + PriceFeedService priceFeedService, + Navigation navigation, + Preferences preferences, + BSFormatter btcFormatter, + BsqFormatter bsqFormatter) { + super(dataModel, + fiatVolumeValidator, + fiatPriceValidator, + altcoinValidator, + btcValidator, + bsqValidator, + securityDepositValidator, + priceFeedService, + navigation, + preferences, + btcFormatter, + bsqFormatter); syncMinAmountWithAmount = false; } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index 906394197b..5ae125f56d 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -144,45 +144,40 @@ public class OpenOffersView extends ActivatableViewAndModel log.debug("Deactivate offer was successful"), (message) -> { log.error(message); new Popup<>().warning(Res.get("offerbook.deactivateOffer.failed", message)).show(); }); - } else { - new Popup<>().information(Res.get("popup.warning.notFullyConnected")).show(); } } private void onActivateOpenOffer(OpenOffer openOffer) { - if (model.isBootstrapped()) { + if (model.isBootstrappedOrShowPopup()) { model.onActivateOpenOffer(openOffer, () -> log.debug("Activate offer was successful"), (message) -> { log.error(message); new Popup<>().warning(Res.get("offerbook.activateOffer.failed", message)).show(); }); - } else { - new Popup<>().information(Res.get("popup.warning.notFullyConnected")).show(); } } private void onRemoveOpenOffer(OpenOffer openOffer) { - if (model.isBootstrapped()) { + if (model.isBootstrappedOrShowPopup()) { String key = "RemoveOfferWarning"; - if (DontShowAgainLookup.showAgain(key)) + if (DontShowAgainLookup.showAgain(key)) { new Popup<>().warning(Res.get("popup.warning.removeOffer", model.formatter.formatCoinWithCode(openOffer.getOffer().getMakerFee()))) .actionButtonText(Res.get("shared.removeOffer")) .onAction(() -> doRemoveOpenOffer(openOffer)) .closeButtonText(Res.get("shared.dontRemoveOffer")) .dontShowAgainId(key) .show(); - else + } else { doRemoveOpenOffer(openOffer); - } else { - new Popup<>().information(Res.get("popup.warning.notFullyConnected")).show(); + } } } @@ -194,12 +189,13 @@ public class OpenOffersView extends ActivatableViewAndModel().instruction(Res.get("offerbook.withdrawFundsHint", Res.get("navigation.funds.availableForWithdrawal"))) .actionButtonTextWithGoTo("navigation.funds.availableForWithdrawal") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class)) .dontShowAgainId(key) .show(); + } }, (message) -> { log.error(message); @@ -208,10 +204,8 @@ public class OpenOffersView extends ActivatableViewAndModel().information(Res.get("popup.warning.notFullyConnected")).show(); } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java index 06959d78f4..8262b72301 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java @@ -19,6 +19,7 @@ package bisq.desktop.main.portfolio.openoffer; import bisq.desktop.common.model.ActivatableWithDataModel; import bisq.desktop.common.model.ViewModel; +import bisq.desktop.util.GUIUtil; import bisq.core.locale.Res; import bisq.core.monetary.Price; @@ -116,7 +117,7 @@ class OpenOffersViewModel extends ActivatableWithDataModel return item != null && item.getOpenOffer() != null && item.getOpenOffer().isDeactivated(); } - boolean isBootstrapped() { - return p2PService.isBootstrapped(); + boolean isBootstrappedOrShowPopup() { + return GUIUtil.isBootstrappedOrShowPopup(p2PService); } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/BuyerSubView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/BuyerSubView.java index 365e51627f..aee1e70c63 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/BuyerSubView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/BuyerSubView.java @@ -27,6 +27,9 @@ import bisq.core.locale.Res; import org.fxmisc.easybind.EasyBind; +import lombok.extern.slf4j.Slf4j; + +@Slf4j public class BuyerSubView extends TradeSubView { private TradeWizardItem step1; private TradeWizardItem step2; @@ -69,6 +72,8 @@ public class BuyerSubView extends TradeSubView { @Override protected void onViewStateChanged(PendingTradesViewModel.State viewState) { + super.onViewStateChanged(viewState); + if (viewState != null) { PendingTradesViewModel.BuyerState buyerState = (PendingTradesViewModel.BuyerState) viewState; diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index 43f187028e..8a9ce8f5d8 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -20,22 +20,26 @@ package bisq.desktop.main.portfolio.pendingtrades; import bisq.desktop.Navigation; import bisq.desktop.common.model.ActivatableDataModel; import bisq.desktop.main.MainView; -import bisq.desktop.main.disputes.DisputesView; import bisq.desktop.main.overlays.notifications.NotificationCenter; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.WalletPasswordWindow; +import bisq.desktop.main.support.SupportView; import bisq.desktop.util.GUIUtil; import bisq.core.account.witness.AccountAgeWitnessService; -import bisq.core.arbitration.Dispute; -import bisq.core.arbitration.DisputeAlreadyOpenException; -import bisq.core.arbitration.DisputeManager; import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeAlreadyOpenException; +import bisq.core.support.dispute.DisputeList; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.traderchat.TraderChatManager; import bisq.core.trade.BuyerTrade; import bisq.core.trade.SellerTrade; import bisq.core.trade.Trade; @@ -44,7 +48,6 @@ import bisq.core.user.Preferences; import bisq.network.p2p.P2PService; -import bisq.common.crypto.KeyRing; import bisq.common.crypto.PubKeyRing; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.FaultHandler; @@ -81,8 +84,8 @@ import static com.google.common.base.Preconditions.checkNotNull; public class PendingTradesDataModel extends ActivatableDataModel { public final TradeManager tradeManager; public final BtcWalletService btcWalletService; - private final KeyRing keyRing; - public final DisputeManager disputeManager; + public final ArbitrationManager arbitrationManager; + public final MediationManager mediationManager; private final P2PService p2PService; private final WalletsSetup walletsSetup; @Getter @@ -97,10 +100,14 @@ public class PendingTradesDataModel extends ActivatableDataModel { final ObjectProperty selectedItemProperty = new SimpleObjectProperty<>(); public final StringProperty txId = new SimpleStringProperty(); + @Getter + private final TraderChatManager traderChatManager; public final Preferences preferences; private boolean activated; private ChangeListener tradeStateChangeListener; private Trade selectedTrade; + @Getter + private PubKeyRing pubKeyRing; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization @@ -109,8 +116,10 @@ public class PendingTradesDataModel extends ActivatableDataModel { @Inject public PendingTradesDataModel(TradeManager tradeManager, BtcWalletService btcWalletService, - KeyRing keyRing, - DisputeManager disputeManager, + PubKeyRing pubKeyRing, + ArbitrationManager arbitrationManager, + MediationManager mediationManager, + TraderChatManager traderChatManager, Preferences preferences, P2PService p2PService, WalletsSetup walletsSetup, @@ -120,8 +129,10 @@ public class PendingTradesDataModel extends ActivatableDataModel { NotificationCenter notificationCenter) { this.tradeManager = tradeManager; this.btcWalletService = btcWalletService; - this.keyRing = keyRing; - this.disputeManager = disputeManager; + this.pubKeyRing = pubKeyRing; + this.arbitrationManager = arbitrationManager; + this.mediationManager = mediationManager; + this.traderChatManager = traderChatManager; this.preferences = preferences; this.p2PService = p2PService; this.walletsSetup = walletsSetup; @@ -174,7 +185,12 @@ public class PendingTradesDataModel extends ActivatableDataModel { ((SellerTrade) getTrade()).onFiatPaymentReceived(resultHandler, errorMessageHandler); } - public void onWithdrawRequest(String toAddress, Coin amount, Coin fee, KeyParameter aesKey, ResultHandler resultHandler, FaultHandler faultHandler) { + public void onWithdrawRequest(String toAddress, + Coin amount, + Coin fee, + KeyParameter aesKey, + ResultHandler resultHandler, + FaultHandler faultHandler) { checkNotNull(getTrade(), "trade must not be null"); if (toAddress != null && toAddress.length() > 0) { @@ -213,11 +229,6 @@ public class PendingTradesDataModel extends ActivatableDataModel { // Getters /////////////////////////////////////////////////////////////////////////////////////////// - @Nullable - public PendingTradesListItem getSelectedItem() { - return selectedItemProperty.get() != null ? selectedItemProperty.get() : null; - } - @Nullable public Trade getTrade() { return selectedItemProperty.get() != null ? selectedItemProperty.get().getTrade() : null; @@ -228,7 +239,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { return getTrade() != null ? getTrade().getOffer() : null; } - boolean isBuyOffer() { + private boolean isBuyOffer() { return getOffer() != null && getOffer().getDirection() == OfferPayload.Direction.BUY; } @@ -250,10 +261,15 @@ public class PendingTradesDataModel extends ActivatableDataModel { if (trade != null) { Offer offer = trade.getOffer(); if (isMaker()) { - if (offer.isCurrencyForMakerFeeBtc()) - return offer.getMakerFee(); - else - return Coin.ZERO;// getTradeFeeAsBsq is used for BSQ + if (offer != null) { + if (offer.isCurrencyForMakerFeeBtc()) + return offer.getMakerFee(); + else + return Coin.ZERO;// getTradeFeeAsBsq is used for BSQ + } else { + log.error("offer is null"); + return Coin.ZERO; + } } else { if (trade.isCurrencyForTakerFeeBtc()) return trade.getTakerFee(); @@ -271,10 +287,15 @@ public class PendingTradesDataModel extends ActivatableDataModel { if (trade != null) { if (isMaker()) { Offer offer = trade.getOffer(); - if (offer.isCurrencyForMakerFeeBtc()) - return offer.getTxFee(); - else - return offer.getTxFee().subtract(offer.getMakerFee()); // BSQ will be used as part of the miner fee + if (offer != null) { + if (offer.isCurrencyForMakerFeeBtc()) + return offer.getTxFee(); + else + return offer.getTxFee().subtract(offer.getMakerFee()); // BSQ will be used as part of the miner fee + } else { + log.error("offer is null"); + return Coin.ZERO; + } } else { if (trade.isCurrencyForTakerFeeBtc()) return trade.getTxFee().multiply(3); @@ -292,10 +313,16 @@ public class PendingTradesDataModel extends ActivatableDataModel { if (trade != null) { if (isMaker()) { Offer offer = trade.getOffer(); - if (offer.isCurrencyForMakerFeeBtc()) - return Coin.ZERO; // getTradeFeeInBTC is used for BTC - else - return offer.getMakerFee(); + if (offer != null) { + if (offer.isCurrencyForMakerFeeBtc()) { + return Coin.ZERO; // getTradeFeeInBTC is used for BTC + } else { + return offer.getMakerFee(); + } + } else { + log.error("offer is null"); + return Coin.ZERO; + } } else { if (trade.isCurrencyForTakerFeeBtc()) return Coin.ZERO; // getTradeFeeInBTC is used for BTC @@ -312,10 +339,6 @@ public class PendingTradesDataModel extends ActivatableDataModel { return getOffer() != null ? getOffer().getCurrencyCode() : ""; } - public OfferPayload.Direction getDirection(Offer offer) { - isMaker = tradeManager.isMyOffer(offer); - return isMaker ? offer.getDirection() : offer.getMirroredDirection(); - } @Nullable public PaymentAccountPayload getSellersPaymentAccountPayload() { @@ -337,15 +360,6 @@ public class PendingTradesDataModel extends ActivatableDataModel { return getOffer() != null ? getOffer().getShortId() : ""; } - public boolean isReadyForTxBroadcast() { - return GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup); - } - - public void showNotReadyForTxBroadcastPopups() { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); - } - - /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// @@ -370,8 +384,9 @@ public class PendingTradesDataModel extends ActivatableDataModel { } private void selectItemByTradeId(String tradeId) { - if (activated) + if (activated) { list.stream().filter(e -> e.getTrade().getId().equals(tradeId)).findAny().ifPresent(this::doSelectItem); + } } private void doSelectItem(@Nullable PendingTradesListItem item) { @@ -380,20 +395,37 @@ public class PendingTradesDataModel extends ActivatableDataModel { if (item != null) { selectedTrade = item.getTrade(); + if (selectedTrade == null) { + log.error("selectedTrade is null"); + return; + } + + Transaction depositTx = selectedTrade.getDepositTx(); + String tradeId = selectedTrade.getId(); tradeStateChangeListener = (observable, oldValue, newValue) -> { - if (selectedTrade.getDepositTx() != null) { - txId.set(selectedTrade.getDepositTx().getHashAsString()); - notificationCenter.setSelectedTradeId(selectedTrade.getId()); + if (depositTx != null) { + txId.set(depositTx.getHashAsString()); + notificationCenter.setSelectedTradeId(tradeId); selectedTrade.stateProperty().removeListener(tradeStateChangeListener); + } else { + txId.set(""); } }; selectedTrade.stateProperty().addListener(tradeStateChangeListener); - isMaker = tradeManager.isMyOffer(selectedTrade.getOffer()); - if (selectedTrade.getDepositTx() != null) - txId.set(selectedTrade.getDepositTx().getHashAsString()); - else + + Offer offer = selectedTrade.getOffer(); + if (offer == null) { + log.error("offer is null"); + return; + } + + isMaker = tradeManager.isMyOffer(offer); + if (depositTx != null) { + txId.set(depositTx.getHashAsString()); + } else { txId.set(""); - notificationCenter.setSelectedTradeId(selectedTrade.getId()); + } + notificationCenter.setSelectedTradeId(tradeId); } else { selectedTrade = null; txId.set(""); @@ -403,78 +435,125 @@ public class PendingTradesDataModel extends ActivatableDataModel { } private void tryOpenDispute(boolean isSupportTicket) { - if (getTrade() != null) { - Transaction depositTx = getTrade().getDepositTx(); - if (depositTx != null) { - doOpenDispute(isSupportTicket, getTrade().getDepositTx()); - } else { - log.info("Trade.depositTx is null. We try to find the tx in our wallet."); - List candidates = new ArrayList<>(); - List transactions = btcWalletService.getRecentTransactions(100, true); - transactions.stream().forEach(transaction -> { - Coin valueSentFromMe = btcWalletService.getValueSentFromMeForTransaction(transaction); - if (!valueSentFromMe.isZero()) { - // spending tx - // MS tx - candidates.addAll(transaction.getOutputs().stream() - .filter(output -> !btcWalletService.isTransactionOutputMine(output)) - .filter(output -> output.getScriptPubKey().isPayToScriptHash()) - .map(transactionOutput -> transaction) - .collect(Collectors.toList())); - } - }); - - if (candidates.size() > 0) { - log.error("Trade.depositTx is null. We take the first possible MultiSig tx just to be able to open a dispute. " + - "candidates={}", candidates); - doOpenDispute(isSupportTicket, candidates.get(0)); - }/* else if (candidates.size() > 1) { - // Let remove that as it confused users and was from little help - new SelectDepositTxWindow().transactions(candidates) - .onSelect(transaction -> doOpenDispute(isSupportTicket, transaction)) - .closeButtonText(Res.get("shared.cancel")) - .show(); - }*/ else if (transactions.size() > 0) { - doOpenDispute(isSupportTicket, transactions.get(0)); - log.error("Trade.depositTx is null and we did not find any MultiSig transaction. We take any random tx just to be able to open a dispute"); - } else { - log.error("Trade.depositTx is null and we did not find any transaction."); - } - } - } else { + Trade trade = getTrade(); + if (trade == null) { log.error("Trade is null"); + return; + } + + Transaction depositTx = trade.getDepositTx(); + if (depositTx != null) { + doOpenDispute(isSupportTicket, depositTx); + } else { + //TODO consider to remove that + log.info("Trade.depositTx is null. We try to find the tx in our wallet."); + List candidates = new ArrayList<>(); + List transactions = btcWalletService.getRecentTransactions(100, true); + transactions.forEach(transaction -> { + Coin valueSentFromMe = btcWalletService.getValueSentFromMeForTransaction(transaction); + if (!valueSentFromMe.isZero()) { + // spending tx + // MS tx + candidates.addAll(transaction.getOutputs().stream() + .filter(output -> !btcWalletService.isTransactionOutputMine(output)) + .filter(output -> output.getScriptPubKey().isPayToScriptHash()) + .map(transactionOutput -> transaction) + .collect(Collectors.toList())); + } + }); + + if (candidates.size() > 0) { + log.error("Trade.depositTx is null. We take the first possible MultiSig tx just to be able to open a dispute. " + + "candidates={}", candidates); + doOpenDispute(isSupportTicket, candidates.get(0)); + } else if (transactions.size() > 0) { + doOpenDispute(isSupportTicket, transactions.get(0)); + log.error("Trade.depositTx is null and we did not find any MultiSig transaction. We take any random tx just to be able to open a dispute"); + } else { + log.error("Trade.depositTx is null and we did not find any transaction."); + } } } private void doOpenDispute(boolean isSupportTicket, Transaction depositTx) { - byte[] depositTxSerialized = null; - byte[] payoutTxSerialized = null; - String depositTxHashAsString = null; - String payoutTxHashAsString = null; - if (depositTx != null) { - depositTxSerialized = depositTx.bitcoinSerialize(); - depositTxHashAsString = depositTx.getHashAsString(); - } else { - log.warn("depositTx is null"); - } Trade trade = getTrade(); - if (trade != null) { - Transaction payoutTx = trade.getPayoutTx(); - if (payoutTx != null) { - payoutTxSerialized = payoutTx.bitcoinSerialize(); - payoutTxHashAsString = payoutTx.getHashAsString(); - } else { - log.debug("payoutTx is null at doOpenDispute"); - } + if (trade == null) { + log.warn("trade is null at doOpenDispute"); + return; + } - final PubKeyRing arbitratorPubKeyRing = trade.getArbitratorPubKeyRing(); - checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must no tbe null"); - Dispute dispute = new Dispute(disputeManager.getDisputeStorage(), + Offer offer = trade.getOffer(); + if (offer == null) { + log.warn("offer is null at doOpenDispute"); + return; + } + + if (!GUIUtil.isBootstrappedOrShowPopup(p2PService)) { + return; + } + + byte[] payoutTxSerialized = null; + String payoutTxHashAsString = null; + Transaction payoutTx = trade.getPayoutTx(); + if (payoutTx != null) { + payoutTxSerialized = payoutTx.bitcoinSerialize(); + payoutTxHashAsString = payoutTx.getHashAsString(); + } + Trade.DisputeState disputeState = trade.getDisputeState(); + DisputeManager> disputeManager; + boolean useMediation; + boolean useArbitration; + // If mediation is not activated we use arbitration + if (MediationManager.isMediationActivated()) { + // In case we re-open a dispute we allow Trade.DisputeState.MEDIATION_REQUESTED or + useMediation = disputeState == Trade.DisputeState.NO_DISPUTE || disputeState == Trade.DisputeState.MEDIATION_REQUESTED; + // in case of arbitration disputeState == Trade.DisputeState.ARBITRATION_REQUESTED + useArbitration = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.DISPUTE_REQUESTED; + } else { + useMediation = false; + useArbitration = true; + } + if (useMediation) { + // If no dispute state set we start with mediation + disputeManager = mediationManager; + PubKeyRing mediatorPubKeyRing = trade.getMediatorPubKeyRing(); + checkNotNull(mediatorPubKeyRing, "mediatorPubKeyRing must not be null"); + byte[] depositTxSerialized = depositTx.bitcoinSerialize(); + String depositTxHashAsString = depositTx.getHashAsString(); + Dispute dispute = new Dispute(disputeManager.getStorage(), trade.getId(), - keyRing.getPubKeyRing().hashCode(), // traderId - trade.getOffer().getDirection() == OfferPayload.Direction.BUY ? isMaker : !isMaker, + pubKeyRing.hashCode(), // traderId + (offer.getDirection() == OfferPayload.Direction.BUY) == isMaker, isMaker, - keyRing.getPubKeyRing(), + pubKeyRing, + trade.getDate().getTime(), + trade.getContract(), + trade.getContractHash(), + depositTxSerialized, + payoutTxSerialized, + depositTxHashAsString, + payoutTxHashAsString, + trade.getContractAsJson(), + trade.getMakerContractSignature(), + trade.getTakerContractSignature(), + mediatorPubKeyRing, + isSupportTicket); + + trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED); + sendOpenNewDisputeMessage(dispute, false, disputeManager); + } else if (useArbitration) { + // Only if we have completed mediation we allow arbitration + disputeManager = arbitrationManager; + PubKeyRing arbitratorPubKeyRing = trade.getArbitratorPubKeyRing(); + checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null"); + byte[] depositTxSerialized = depositTx.bitcoinSerialize(); + String depositTxHashAsString = depositTx.getHashAsString(); + Dispute dispute = new Dispute(disputeManager.getStorage(), + trade.getId(), + pubKeyRing.hashCode(), // traderId + (offer.getDirection() == OfferPayload.Direction.BUY) == isMaker, + isMaker, + pubKeyRing, trade.getDate().getTime(), trade.getContract(), trade.getContractHash(), @@ -486,30 +565,27 @@ public class PendingTradesDataModel extends ActivatableDataModel { trade.getMakerContractSignature(), trade.getTakerContractSignature(), arbitratorPubKeyRing, - isSupportTicket - ); + isSupportTicket); trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); - if (p2PService.isBootstrapped()) { - sendOpenNewDisputeMessage(dispute, false); - } else { - new Popup<>().information(Res.get("popup.warning.notFullyConnected")).show(); - } + sendOpenNewDisputeMessage(dispute, false, disputeManager); } else { - log.warn("trade is null at doOpenDispute"); + log.warn("Invalid dispute state {}", disputeState.name()); } } - private void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen) { + private void sendOpenNewDisputeMessage(Dispute dispute, + boolean reOpen, + DisputeManager> disputeManager) { disputeManager.sendOpenNewDisputeMessage(dispute, reOpen, - () -> navigation.navigateTo(MainView.class, DisputesView.class), + () -> navigation.navigateTo(MainView.class, SupportView.class), (errorMessage, throwable) -> { if ((throwable instanceof DisputeAlreadyOpenException)) { errorMessage += "\n\n" + Res.get("portfolio.pending.openAgainDispute.msg"); new Popup<>().warning(errorMessage) .actionButtonText(Res.get("portfolio.pending.openAgainDispute.button")) - .onAction(() -> sendOpenNewDisputeMessage(dispute, true)) + .onAction(() -> sendOpenNewDisputeMessage(dispute, true, disputeManager)) .closeButtonText(Res.get("shared.cancel")) .show(); } else { @@ -517,5 +593,13 @@ public class PendingTradesDataModel extends ActivatableDataModel { } }); } + + public boolean isReadyForTxBroadcast() { + return GUIUtil.isReadyForTxBroadcastOrShowPopup(p2PService, walletsSetup); + } + + public boolean isBootstrappedOrShowPopup() { + return GUIUtil.isBootstrappedOrShowPopup(p2PService); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index bbe3ec7881..386edd104e 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -22,19 +22,20 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.PeerInfoIcon; -import bisq.desktop.main.Chat.Chat; import bisq.desktop.main.MainView; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; -import bisq.desktop.util.FormBuilder; +import bisq.desktop.main.shared.ChatView; import bisq.desktop.util.CssTheme; +import bisq.desktop.util.FormBuilder; import bisq.core.alert.PrivateNotificationManager; import bisq.core.app.AppOptionKeys; -import bisq.core.arbitration.messages.DisputeCommunicationMessage; import bisq.core.locale.Res; +import bisq.core.support.messages.ChatMessage; +import bisq.core.support.traderchat.TradeChatSession; +import bisq.core.support.traderchat.TraderChatManager; import bisq.core.trade.Trade; -import bisq.core.trade.TradeChatSession; import bisq.core.user.Preferences; import bisq.core.util.BSFormatter; @@ -122,8 +123,8 @@ public class PendingTradesView extends ActivatableViewAndModel buttonByTrade = new HashMap<>(); private Map badgeByTrade = new HashMap<>(); - private Map> listenerByTrade = new HashMap<>(); - private TradeChatSession.DisputeStateListener disputeStateListener; + private Map> listenerByTrade = new HashMap<>(); + private TraderChatManager.DisputeStateListener disputeStateListener; /////////////////////////////////////////////////////////////////////////////////////////// @@ -318,7 +319,7 @@ public class PendingTradesView extends ActivatableViewAndModel { Trade trade = t.getTrade(); newChatMessagesByTradeMap.put(trade.getId(), - trade.getCommunicationMessages().stream() + trade.getChatMessages().stream() .filter(m -> !m.isWasDisplayed()) .filter(m -> !m.isSystemMessage()) .count()); @@ -329,43 +330,41 @@ public class PendingTradesView extends ActivatableViewAndModel m.setWasDisplayed(true)); + trade.getChatMessages().forEach(m -> m.setWasDisplayed(true)); trade.persist(); tradeIdOfOpenChat = trade.getId(); - Chat tradeChat = new Chat(model.dataModel.tradeManager.getChatManager(), formatter); - tradeChat.setAllowAttachments(false); - tradeChat.setDisplayHeader(false); - tradeChat.initialize(); + ChatView chatView = new ChatView(traderChatManager, formatter); + chatView.setAllowAttachments(false); + chatView.setDisplayHeader(false); + chatView.initialize(); - AnchorPane pane = new AnchorPane(tradeChat); + AnchorPane pane = new AnchorPane(chatView); pane.setPrefSize(760, 500); - AnchorPane.setLeftAnchor(tradeChat, 10d); - AnchorPane.setRightAnchor(tradeChat, 10d); - AnchorPane.setTopAnchor(tradeChat, -20d); - AnchorPane.setBottomAnchor(tradeChat, 10d); + AnchorPane.setLeftAnchor(chatView, 10d); + AnchorPane.setRightAnchor(chatView, 10d); + AnchorPane.setTopAnchor(chatView, -20d); + AnchorPane.setBottomAnchor(chatView, 10d); boolean isTaker = !model.dataModel.isMaker(trade.getOffer()); - boolean isBuyer = model.dataModel.isBuyer(); - TradeChatSession chatSession = new TradeChatSession(trade, isTaker, isBuyer, - model.dataModel.tradeManager, - model.dataModel.tradeManager.getChatManager()); + TradeChatSession tradeChatSession = new TradeChatSession(trade, isTaker); disputeStateListener = tradeId -> { if (trade.getId().equals(tradeId)) { chatPopupStage.hide(); } }; - chatSession.addDisputeStateListener(disputeStateListener); + traderChatManager.addDisputeStateListener(disputeStateListener); - tradeChat.display(chatSession, null, pane.widthProperty()); + chatView.display(tradeChatSession, pane.widthProperty()); - tradeChat.activate(); - tradeChat.scrollToBottom(); + chatView.activate(); + chatView.scrollToBottom(); chatPopupStage = new Stage(); chatPopupStage.setTitle(Res.get("tradeChat.chatWindowTitle", trade.getShortId())); @@ -375,9 +374,9 @@ public class PendingTradesView extends ActivatableViewAndModel { - tradeChat.deactivate(); + chatView.deactivate(); // at close we set all as displayed. While open we ignore updates of the numNewMsg in the list icon. - trade.getCommunicationMessages().forEach(m -> m.setWasDisplayed(true)); + trade.getChatMessages().forEach(m -> m.setWasDisplayed(true)); trade.persist(); tradeIdOfOpenChat = null; @@ -388,7 +387,7 @@ public class PendingTradesView extends ActivatableViewAndModel listener = c -> update(trade, badge); + ListChangeListener listener = c -> update(trade, badge); listenerByTrade.put(id, listener); - trade.getCommunicationMessages().addListener(listener); + trade.getChatMessages().addListener(listener); } update(trade, badge); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/SellerSubView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/SellerSubView.java index aac57aea6c..33d6990016 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/SellerSubView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/SellerSubView.java @@ -27,6 +27,9 @@ import bisq.core.locale.Res; import org.fxmisc.easybind.EasyBind; +import lombok.extern.slf4j.Slf4j; + +@Slf4j public class SellerSubView extends TradeSubView { private TradeWizardItem step1; private TradeWizardItem step2; @@ -37,7 +40,7 @@ public class SellerSubView extends TradeSubView { // Constructor, Initialisation /////////////////////////////////////////////////////////////////////////////////////////// - public SellerSubView(PendingTradesViewModel model) { + SellerSubView(PendingTradesViewModel model) { super(model); } @@ -69,6 +72,8 @@ public class SellerSubView extends TradeSubView { @Override protected void onViewStateChanged(PendingTradesViewModel.State viewState) { + super.onViewStateChanged(viewState); + if (viewState != null) { PendingTradesViewModel.SellerState sellerState = (PendingTradesViewModel.SellerState) viewState; diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeStepInfo.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeStepInfo.java new file mode 100644 index 0000000000..6e75041d31 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeStepInfo.java @@ -0,0 +1,196 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.main.portfolio.pendingtrades; + +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.TitledGroupBg; + +import bisq.core.locale.Res; +import bisq.core.trade.Trade; + +import javafx.scene.control.Label; +import javafx.scene.layout.GridPane; + +import javafx.event.ActionEvent; +import javafx.event.EventHandler; + +import java.util.function.Supplier; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +@Slf4j +public class TradeStepInfo { + + public enum State { + UNDEFINED, + SHOW_GET_HELP_BUTTON, + IN_MEDIATION_SELF_REQUESTED, + IN_MEDIATION_PEER_REQUESTED, + MEDIATION_RESULT, + MEDIATION_RESULT_SELF_ACCEPTED, + MEDIATION_RESULT_PEER_ACCEPTED, + IN_ARBITRATION_SELF_REQUESTED, + IN_ARBITRATION_PEER_REQUESTED, + WARN_HALF_PERIOD, + WARN_PERIOD_OVER + } + + private final TitledGroupBg titledGroupBg; + private final Label label; + private final AutoTooltipButton button; + @Nullable + @Setter + private Trade trade; + @Getter + private State state = State.UNDEFINED; + private Supplier fistHalfOverWarnTextSupplier = () -> ""; + private Supplier periodOverWarnTextSupplier = () -> ""; + + TradeStepInfo(TitledGroupBg titledGroupBg, Label label, AutoTooltipButton button) { + this.titledGroupBg = titledGroupBg; + this.label = label; + this.button = button; + GridPane.setColumnIndex(button, 0); + + setState(State.SHOW_GET_HELP_BUTTON); + } + + void removeItselfFrom(GridPane leftGridPane) { + leftGridPane.getChildren().remove(titledGroupBg); + leftGridPane.getChildren().remove(label); + leftGridPane.getChildren().remove(button); + } + + public void setOnAction(EventHandler e) { + button.setOnAction(e); + } + + public void setFistHalfOverWarnTextSupplier(Supplier fistHalfOverWarnTextSupplier) { + this.fistHalfOverWarnTextSupplier = fistHalfOverWarnTextSupplier; + } + + public void setPeriodOverWarnTextSupplier(Supplier periodOverWarnTextSupplier) { + this.periodOverWarnTextSupplier = periodOverWarnTextSupplier; + } + + public void setState(State state) { + this.state = state; + switch (state) { + case UNDEFINED: + break; + case SHOW_GET_HELP_BUTTON: + // grey button + titledGroupBg.setText(Res.get("portfolio.pending.support.headline.getHelp")); + label.setText(Res.get("portfolio.pending.support.text.getHelp")); + button.setText(Res.get("portfolio.pending.support.button.getHelp").toUpperCase()); + button.setId(null); + button.getStyleClass().remove("action-button"); + button.setDisable(false); + break; + case IN_MEDIATION_SELF_REQUESTED: + // red button + titledGroupBg.setText(Res.get("portfolio.pending.mediationRequested")); + label.setText(Res.get("portfolio.pending.disputeOpenedMyUser", Res.get("portfolio.pending.communicateWithMediator"))); + button.setText(Res.get("portfolio.pending.mediationRequested").toUpperCase()); + button.setId("open-dispute-button"); + button.getStyleClass().remove("action-button"); + button.setDisable(true); + break; + case IN_MEDIATION_PEER_REQUESTED: + // red button + titledGroupBg.setText(Res.get("portfolio.pending.mediationRequested")); + label.setText(Res.get("portfolio.pending.disputeOpenedByPeer", Res.get("portfolio.pending.communicateWithMediator"))); + button.setText(Res.get("portfolio.pending.mediationRequested").toUpperCase()); + button.setId("open-dispute-button"); + button.getStyleClass().remove("action-button"); + button.setDisable(true); + break; + case MEDIATION_RESULT: + // green button + titledGroupBg.setText(Res.get("portfolio.pending.mediationResult.headline")); + label.setText(Res.get("portfolio.pending.mediationResult.info.noneAccepted")); + button.setText(Res.get("portfolio.pending.mediationResult.button").toUpperCase()); + button.setId(null); + button.getStyleClass().add("action-button"); + button.setDisable(false); + break; + case MEDIATION_RESULT_SELF_ACCEPTED: + // green button deactivated + titledGroupBg.setText(Res.get("portfolio.pending.mediationResult.headline")); + label.setText(Res.get("portfolio.pending.mediationResult.info.selfAccepted")); + button.setText(Res.get("portfolio.pending.mediationResult.button").toUpperCase()); + button.setId(null); + button.getStyleClass().add("action-button"); + button.setDisable(false); + break; + case MEDIATION_RESULT_PEER_ACCEPTED: + // green button + titledGroupBg.setText(Res.get("portfolio.pending.mediationResult.headline")); + label.setText(Res.get("portfolio.pending.mediationResult.info.peerAccepted")); + button.setText(Res.get("portfolio.pending.mediationResult.button").toUpperCase()); + button.setId(null); + button.getStyleClass().add("action-button"); + button.setDisable(false); + break; + case IN_ARBITRATION_SELF_REQUESTED: + // red button + titledGroupBg.setText(Res.get("portfolio.pending.arbitrationRequested")); + label.setText(Res.get("portfolio.pending.disputeOpenedMyUser", Res.get("portfolio.pending.communicateWithArbitrator"))); + button.setText(Res.get("portfolio.pending.arbitrationRequested").toUpperCase()); + button.setId("open-dispute-button"); + button.getStyleClass().remove("action-button"); + button.setDisable(true); + break; + case IN_ARBITRATION_PEER_REQUESTED: + // red button + titledGroupBg.setText(Res.get("portfolio.pending.arbitrationRequested")); + label.setText(Res.get("portfolio.pending.disputeOpenedByPeer", Res.get("portfolio.pending.communicateWithArbitrator"))); + button.setText(Res.get("portfolio.pending.arbitrationRequested").toUpperCase()); + button.setId("open-dispute-button"); + button.getStyleClass().remove("action-button"); + button.setDisable(true); + break; + case WARN_HALF_PERIOD: + // orange button + titledGroupBg.setText(Res.get("portfolio.pending.support.headline.halfPeriodOver")); + label.setText(fistHalfOverWarnTextSupplier.get()); + button.setText(Res.get("portfolio.pending.openSupport").toUpperCase()); + button.setId(null); + button.getStyleClass().remove("action-button"); + button.setDisable(false); + break; + case WARN_PERIOD_OVER: + // red button + titledGroupBg.setText(Res.get("portfolio.pending.support.headline.periodOver")); + label.setText(periodOverWarnTextSupplier.get()); + button.setText(Res.get("portfolio.pending.openSupport").toUpperCase()); + button.setId("open-dispute-button"); + button.getStyleClass().remove("action-button"); + button.setDisable(false); + break; + } + + if (trade != null && trade.getPayoutTx() != null) { + button.setDisable(true); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java index 33c9d54859..84b6b81aa7 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/TradeSubView.java @@ -39,26 +39,24 @@ import javafx.geometry.Orientation; import org.fxmisc.easybind.Subscription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; import static bisq.desktop.util.FormBuilder.addMultilineLabel; import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +@Slf4j public abstract class TradeSubView extends HBox { - protected final Logger log = LoggerFactory.getLogger(this.getClass()); - protected final PendingTradesViewModel model; protected VBox leftVBox; private AnchorPane contentPane; private TradeStepView tradeStepView; - private AutoTooltipButton openDisputeButton; - private NotificationGroup notificationGroup; + protected TradeStepInfo tradeStepInfo; private GridPane leftGridPane; private TitledGroupBg tradeProcessTitledGroupBg; private int leftGridPaneRowIndex = 0; - protected Subscription viewStateSubscription; + Subscription viewStateSubscription; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, Initialisation @@ -81,11 +79,8 @@ public abstract class TradeSubView extends HBox { if (tradeStepView != null) tradeStepView.deactivate(); - if (openDisputeButton != null) - leftGridPane.getChildren().remove(openDisputeButton); - - if (notificationGroup != null) - notificationGroup.removeItselfFrom(leftGridPane); + if (tradeStepInfo != null) + tradeStepInfo.removeItselfFrom(leftGridPane); } private void buildViews() { @@ -105,60 +100,25 @@ public abstract class TradeSubView extends HBox { addWizards(); - TitledGroupBg noticeTitledGroupBg = addTitledGroupBg(leftGridPane, leftGridPaneRowIndex, 1, "", - 0); - noticeTitledGroupBg.getStyleClass().add("last"); - Label label = addMultilineLabel(leftGridPane, leftGridPaneRowIndex, "", - Layout.FIRST_ROW_DISTANCE); - openDisputeButton = (AutoTooltipButton) addButtonAfterGroup(leftGridPane, ++leftGridPaneRowIndex, Res.get("portfolio.pending.openDispute")); - GridPane.setColumnIndex(openDisputeButton, 0); - openDisputeButton.setId("open-dispute-button"); - - notificationGroup = new NotificationGroup(noticeTitledGroupBg, label, openDisputeButton); - notificationGroup.setLabelAndHeadlineVisible(false); - notificationGroup.setButtonVisible(false); + TitledGroupBg titledGroupBg = addTitledGroupBg(leftGridPane, leftGridPaneRowIndex, 1, "", 30); + titledGroupBg.getStyleClass().add("last"); + Label label = addMultilineLabel(leftGridPane, leftGridPaneRowIndex, "", 30); + AutoTooltipButton button = (AutoTooltipButton) addButtonAfterGroup(leftGridPane, ++leftGridPaneRowIndex, ""); + tradeStepInfo = new TradeStepInfo(titledGroupBg, label, button); } - public static class NotificationGroup { - public final TitledGroupBg titledGroupBg; - public final Label label; - public final AutoTooltipButton button; - - public NotificationGroup(TitledGroupBg titledGroupBg, Label label, AutoTooltipButton button) { - this.titledGroupBg = titledGroupBg; - this.label = label; - this.button = button; - } - - public void setLabelAndHeadlineVisible(boolean isVisible) { - titledGroupBg.setVisible(isVisible); - label.setVisible(isVisible); - titledGroupBg.setManaged(isVisible); - label.setManaged(isVisible); - } - - public void setButtonVisible(boolean isVisible) { - button.setVisible(isVisible); - button.setManaged(isVisible); - } - - public void removeItselfFrom(GridPane leftGridPane) { - leftGridPane.getChildren().remove(titledGroupBg); - leftGridPane.getChildren().remove(label); - leftGridPane.getChildren().remove(button); - } - } - - protected void showItem(TradeWizardItem item) { + void showItem(TradeWizardItem item) { item.setActive(); createAndAddTradeStepView(item.getViewClass()); } - abstract protected void addWizards(); + protected abstract void addWizards(); - abstract protected void onViewStateChanged(PendingTradesViewModel.State viewState); + protected void onViewStateChanged(PendingTradesViewModel.State viewState) { + tradeStepInfo.setTrade(model.getTrade()); + } - protected void addWizardsToGridPane(TradeWizardItem tradeWizardItem) { + void addWizardsToGridPane(TradeWizardItem tradeWizardItem) { if (leftGridPaneRowIndex == 0) GridPane.setMargin(tradeWizardItem, new Insets(Layout.FIRST_ROW_DISTANCE + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); @@ -168,7 +128,7 @@ public abstract class TradeSubView extends HBox { GridPane.setFillWidth(tradeWizardItem, true); } - protected void addLineSeparatorToGridPane() { + void addLineSeparatorToGridPane() { final Separator separator = new Separator(Orientation.VERTICAL); separator.setMinHeight(22); GridPane.setMargin(separator, new Insets(0, 0, 0, 13)); @@ -183,7 +143,7 @@ public abstract class TradeSubView extends HBox { try { tradeStepView = viewClass.getDeclaredConstructor(PendingTradesViewModel.class).newInstance(model); contentPane.getChildren().setAll(tradeStepView); - tradeStepView.setNotificationGroup(notificationGroup); + tradeStepView.setTradeStepInfo(tradeStepInfo); tradeStepView.activate(); } catch (Exception e) { log.error("Creating viewClass {} caused an error {}", viewClass, e.getMessage()); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index 809b5302ea..c9907478b9 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -20,28 +20,38 @@ package bisq.desktop.main.portfolio.pendingtrades.steps; import bisq.desktop.components.InfoTextField; import bisq.desktop.components.TitledGroupBg; import bisq.desktop.components.TxIdTextField; +import bisq.desktop.main.overlays.Overlay; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; -import bisq.desktop.main.portfolio.pendingtrades.TradeSubView; +import bisq.desktop.main.portfolio.pendingtrades.TradeStepInfo; import bisq.desktop.util.Layout; -import bisq.core.arbitration.Dispute; import bisq.core.locale.Res; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.trade.Contract; import bisq.core.trade.Trade; +import bisq.core.user.DontShowAgainLookup; import bisq.core.user.Preferences; import bisq.common.ClockWatcher; +import bisq.common.UserThread; +import bisq.common.app.Version; import bisq.common.util.Tuple3; +import bisq.common.util.Utilities; import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; import com.jfoenix.controls.JFXProgressBar; +import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; @@ -56,6 +66,8 @@ import org.fxmisc.easybind.Subscription; import javafx.beans.value.ChangeListener; +import javafx.event.EventHandler; + import java.util.Optional; import org.slf4j.Logger; @@ -76,18 +88,21 @@ public abstract class TradeStepView extends AnchorPane { protected final Preferences preferences; protected final GridPane gridPane; - private Subscription disputeStateSubscription; - private Subscription tradePeriodStateSubscription; + private Subscription tradePeriodStateSubscription, disputeStateSubscription, mediationResultStateSubscription; protected int gridRow = 0; - protected TitledGroupBg tradeInfoTitledGroupBg; private TextField timeLeftTextField; private ProgressBar timeLeftProgressBar; private TxIdTextField txIdTextField; - protected TradeSubView.NotificationGroup notificationGroup; + private TradeStepInfo tradeStepInfo; private Subscription txIdSubscription; private ClockWatcher.Listener clockListener; private final ChangeListener errorMessageListener; protected Label infoLabel; + private Overlay acceptMediationResultPopup; + + // TODO remove before release. Only in for making dev testing easier + private EventHandler keyEventEventHandler; + private Scene scene; /////////////////////////////////////////////////////////////////////////////////////////// @@ -136,7 +151,7 @@ public abstract class TradeStepView extends AnchorPane { errorMessageListener = (observable, oldValue, newValue) -> { if (newValue != null) - showSupportFields(); + new Popup<>().error(newValue).show(); }; clockListener = new ClockWatcher.Listener() { @@ -149,9 +164,39 @@ public abstract class TradeStepView extends AnchorPane { updateTimeLeft(); } }; + + // TODO remove before relase. Only in for making dev testing easier + if (Version.VERSION.equals("1.1.5")) { + keyEventEventHandler = keyEvent -> { + String key; + if (Utilities.isAltOrCtrlPressed(KeyCode.UP, keyEvent)) { + if (trade.getTradePeriodState() == Trade.TradePeriodState.FIRST_HALF) { + trade.setTradePeriodState(Trade.TradePeriodState.SECOND_HALF); + } else if (trade.getTradePeriodState() == Trade.TradePeriodState.SECOND_HALF) { + trade.setTradePeriodState(Trade.TradePeriodState.TRADE_PERIOD_OVER); + } + } else if (Utilities.isAltOrCtrlPressed(KeyCode.DOWN, keyEvent)) { + if (trade.getTradePeriodState() == Trade.TradePeriodState.TRADE_PERIOD_OVER) { + trade.setTradePeriodState(Trade.TradePeriodState.SECOND_HALF); + key = "displayTradePeriodOver" + trade.getId(); + DontShowAgainLookup.dontShowAgain(key, false); + } else if (trade.getTradePeriodState() == Trade.TradePeriodState.SECOND_HALF) { + trade.setTradePeriodState(Trade.TradePeriodState.FIRST_HALF); + key = "displayHalfTradePeriodOver" + trade.getId(); + DontShowAgainLookup.dontShowAgain(key, false); + } + } + }; + } } public void activate() { + // TODO remove before relase. Only in for making dev testing easier + scene = getScene(); + if (Version.VERSION.equals("1.1.5") && scene != null) { + getScene().addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + if (txIdTextField != null) { if (txIdSubscription != null) txIdSubscription.unsubscribe(); @@ -165,11 +210,26 @@ public abstract class TradeStepView extends AnchorPane { } trade.errorMessageProperty().addListener(errorMessageListener); + if (!isMediationClosedState()) { + tradeStepInfo.setOnAction(e -> { + new Popup<>().attention(Res.get("portfolio.pending.support.popup.info")) + .actionButtonText(Res.get("portfolio.pending.support.popup.button")) + .onAction(this::openSupportTicket) + .closeButtonText(Res.get("shared.cancel")) + .show(); + }); + } + disputeStateSubscription = EasyBind.subscribe(trade.disputeStateProperty(), newValue -> { if (newValue != null) updateDisputeState(newValue); }); + mediationResultStateSubscription = EasyBind.subscribe(trade.mediationResultStateProperty(), newValue -> { + if (newValue != null) + updateMediationResultState(); + }); + tradePeriodStateSubscription = EasyBind.subscribe(trade.tradePeriodStateProperty(), newValue -> { if (newValue != null) updateTradePeriodState(newValue); @@ -181,7 +241,17 @@ public abstract class TradeStepView extends AnchorPane { infoLabel.setText(getInfoText()); } + private void openSupportTicket() { + applyOnDisputeOpened(); + model.dataModel.onOpenDispute(); + } + public void deactivate() { + // TODO remove before relase. Only in for making dev testing easier + if (Version.VERSION.equals("1.1.5") && scene != null) { + scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); + } + if (txIdSubscription != null) txIdSubscription.unsubscribe(); @@ -194,14 +264,17 @@ public abstract class TradeStepView extends AnchorPane { if (disputeStateSubscription != null) disputeStateSubscription.unsubscribe(); + if (mediationResultStateSubscription != null) + mediationResultStateSubscription.unsubscribe(); + if (tradePeriodStateSubscription != null) tradePeriodStateSubscription.unsubscribe(); if (clockListener != null) model.clockWatcher.removeListener(clockListener); - if (notificationGroup != null) - notificationGroup.button.setOnAction(null); + if (tradeStepInfo != null) + tradeStepInfo.setOnAction(null); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -214,7 +287,7 @@ public abstract class TradeStepView extends AnchorPane { } protected void addTradeInfoBlock() { - tradeInfoTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 3, + TitledGroupBg tradeInfoTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 3, Res.get("portfolio.pending.tradeInformation")); GridPane.setColumnSpan(tradeInfoTitledGroupBg, 2); @@ -304,189 +377,242 @@ public abstract class TradeStepView extends AnchorPane { // We have the dispute button and text field on the left side, but we handle the content here as it // is trade state specific - public void setNotificationGroup(TradeSubView.NotificationGroup notificationGroup) { - this.notificationGroup = notificationGroup; + public void setTradeStepInfo(TradeStepInfo tradeStepInfo) { + this.tradeStepInfo = tradeStepInfo; + + tradeStepInfo.setFistHalfOverWarnTextSupplier(this::getFistHalfOverWarnText); + tradeStepInfo.setPeriodOverWarnTextSupplier(this::getPeriodOverWarnText); } - private void showDisputeInfoLabel() { - if (notificationGroup != null) - notificationGroup.setLabelAndHeadlineVisible(true); - } - private void showOpenDisputeButton() { - if (notificationGroup != null) { - notificationGroup.setButtonVisible(true); - notificationGroup.button.setOnAction(e -> { - notificationGroup.button.setDisable(true); - onDisputeOpened(); - model.dataModel.onOpenDispute(); - }); - } - } - - protected void setWarningHeadline() { - if (notificationGroup != null) { - notificationGroup.titledGroupBg.setText(Res.get("shared.warning")); - } - } - - protected void setInformationHeadline() { - if (notificationGroup != null) { - notificationGroup.titledGroupBg.setText(Res.get("portfolio.pending.notification")); - } - } - - protected void setOpenDisputeHeadline() { - if (notificationGroup != null) { - notificationGroup.titledGroupBg.setText(Res.get("portfolio.pending.openDispute")); - } - } - - protected void setDisputeOpenedHeadline() { - if (notificationGroup != null) { - notificationGroup.titledGroupBg.setText(Res.get("portfolio.pending.disputeOpened")); - } - } - - protected void setRequestSupportHeadline() { - if (notificationGroup != null) { - notificationGroup.titledGroupBg.setText(Res.get("portfolio.pending.openSupport")); - } - } - - protected void setSupportOpenedHeadline() { - if (notificationGroup != null) { - notificationGroup.titledGroupBg.setText(Res.get("portfolio.pending.supportTicketOpened")); - } - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Support - /////////////////////////////////////////////////////////////////////////////////////////// - - private void showSupportFields() { - if (notificationGroup != null) { - notificationGroup.button.updateText(Res.get("portfolio.pending.requestSupport")); - notificationGroup.button.setId("open-support-button"); - notificationGroup.button.setOnAction(e -> model.dataModel.onOpenSupportTicket()); - } - new Popup<>().warning(trade.errorMessageProperty().getValue() - + "\n\n" + Res.get("portfolio.pending.error.requestSupport")) - .show(); - - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Warning - /////////////////////////////////////////////////////////////////////////////////////////// - - private void showWarning() { - showDisputeInfoLabel(); - - if (notificationGroup != null) - notificationGroup.label.setText(getWarningText()); - } - - private void removeWarning() { - hideNotificationGroup(); - } - - protected String getWarningText() { + protected String getFistHalfOverWarnText() { return ""; } + /////////////////////////////////////////////////////////////////////////////////////////// // Dispute /////////////////////////////////////////////////////////////////////////////////////////// - private void onOpenForDispute() { - showDisputeInfoLabel(); - showOpenDisputeButton(); - setOpenDisputeHeadline(); - - if (notificationGroup != null) - notificationGroup.label.setText(getOpenForDisputeText()); - } - - private void onDisputeOpened() { - showDisputeInfoLabel(); - showOpenDisputeButton(); - applyOnDisputeOpened(); - setDisputeOpenedHeadline(); - - if (notificationGroup != null) - notificationGroup.button.setDisable(true); - } - - protected String getOpenForDisputeText() { + protected String getPeriodOverWarnText() { return ""; } protected void applyOnDisputeOpened() { } - protected void hideNotificationGroup() { - notificationGroup.setLabelAndHeadlineVisible(false); - notificationGroup.setButtonVisible(false); - } - private void updateDisputeState(Trade.DisputeState disputeState) { + deactivatePaymentButtons(false); Optional ownDispute; switch (disputeState) { case NO_DISPUTE: break; case DISPUTE_REQUESTED: - onDisputeOpened(); - ownDispute = model.dataModel.disputeManager.findOwnDispute(trade.getId()); + if (tradeStepInfo != null) { + tradeStepInfo.setFistHalfOverWarnTextSupplier(this::getFistHalfOverWarnText); + } + applyOnDisputeOpened(); + + ownDispute = model.dataModel.arbitrationManager.findOwnDispute(trade.getId()); ownDispute.ifPresent(dispute -> { - String msg; - if (dispute.isSupportTicket()) { - setSupportOpenedHeadline(); - msg = Res.get("portfolio.pending.supportTicketOpenedMyUser", Res.get("portfolio.pending.communicateWithArbitrator")); - } else { - setDisputeOpenedHeadline(); - msg = Res.get("portfolio.pending.disputeOpenedMyUser", Res.get("portfolio.pending.communicateWithArbitrator")); - } - if (notificationGroup != null) - notificationGroup.label.setText(msg); + if (tradeStepInfo != null) + tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED); }); break; case DISPUTE_STARTED_BY_PEER: - onDisputeOpened(); - ownDispute = model.dataModel.disputeManager.findOwnDispute(trade.getId()); + if (tradeStepInfo != null) { + tradeStepInfo.setFistHalfOverWarnTextSupplier(this::getFistHalfOverWarnText); + } + applyOnDisputeOpened(); + + ownDispute = model.dataModel.arbitrationManager.findOwnDispute(trade.getId()); ownDispute.ifPresent(dispute -> { - String msg; - if (dispute.isSupportTicket()) { - setSupportOpenedHeadline(); - msg = Res.get("portfolio.pending.supportTicketOpenedByPeer", Res.get("portfolio.pending.communicateWithArbitrator")); - } else { - setDisputeOpenedHeadline(); - msg = Res.get("portfolio.pending.disputeOpenedByPeer", Res.get("portfolio.pending.communicateWithArbitrator")); - } - if (notificationGroup != null) - notificationGroup.label.setText(msg); + if (tradeStepInfo != null) + tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED); }); break; case DISPUTE_CLOSED: break; + case MEDIATION_REQUESTED: + if (tradeStepInfo != null) { + tradeStepInfo.setFistHalfOverWarnTextSupplier(this::getFistHalfOverWarnText); + } + applyOnDisputeOpened(); + + ownDispute = model.dataModel.mediationManager.findOwnDispute(trade.getId()); + ownDispute.ifPresent(dispute -> { + if (tradeStepInfo != null) + tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_SELF_REQUESTED); + }); + + break; + case MEDIATION_STARTED_BY_PEER: + if (tradeStepInfo != null) { + tradeStepInfo.setFistHalfOverWarnTextSupplier(this::getFistHalfOverWarnText); + } + applyOnDisputeOpened(); + + ownDispute = model.dataModel.mediationManager.findOwnDispute(trade.getId()); + ownDispute.ifPresent(dispute -> { + if (tradeStepInfo != null) { + tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_PEER_REQUESTED); + } + }); + break; + case MEDIATION_CLOSED: + deactivatePaymentButtons(true); + + if (tradeStepInfo != null) { + tradeStepInfo.setOnAction(e -> { + updateMediationResultState(); + }); + } + + if (tradeStepInfo != null) { + tradeStepInfo.setState(TradeStepInfo.State.MEDIATION_RESULT); + } + + updateMediationResultState(); + break; } } + private void updateMediationResultState() { + if (isInArbitration()) { + if (isArbitrationStartedByPeer()) { + tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED); + } else if (isArbitrationSelfStarted()) { + tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED); + } + } else if (isMediationClosedState()) { + // We do not use the state itself as it is not guaranteed the last state reflects relevant information + // (e.g. we might receive a RECEIVED_SIG_MSG but then later a SIG_MSG_IN_MAILBOX). + if (hasSelfAccepted()) { + tradeStepInfo.setState(TradeStepInfo.State.MEDIATION_RESULT_SELF_ACCEPTED); + openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline", trade.getShortId())); + } else if (peerAccepted()) { + tradeStepInfo.setState(TradeStepInfo.State.MEDIATION_RESULT_PEER_ACCEPTED); + if (acceptMediationResultPopup == null) { + openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline.peerAccepted", trade.getShortId())); + } + } else { + tradeStepInfo.setState(TradeStepInfo.State.MEDIATION_RESULT); + openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline", trade.getShortId())); + } + } + } + + private boolean isInArbitration() { + return isArbitrationStartedByPeer() || isArbitrationSelfStarted(); + } + + private boolean isArbitrationStartedByPeer() { + return trade.getDisputeState() == Trade.DisputeState.DISPUTE_STARTED_BY_PEER; + } + + private boolean isArbitrationSelfStarted() { + return trade.getDisputeState() == Trade.DisputeState.DISPUTE_REQUESTED; + } + + private boolean isMediationClosedState() { + return trade.getDisputeState() == Trade.DisputeState.MEDIATION_CLOSED; + } + + private boolean hasSelfAccepted() { + return trade.getProcessModel().getMediatedPayoutTxSignature() != null; + } + + private boolean peerAccepted() { + return trade.getProcessModel().getTradingPeer().getMediatedPayoutTxSignature() != null; + } + + private void openMediationResultPopup(String headLine) { + if (acceptMediationResultPopup != null) { + return; + } + + Optional optionalDispute = model.dataModel.mediationManager.findDispute(trade.getId()); + if (!optionalDispute.isPresent()) { + return; + } + + if (trade.getPayoutTx() != null) { + return; + } + + DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get(); + Contract contract = checkNotNull(trade.getContract(), "contract must not be null"); + boolean isMyRoleBuyer = contract.isMyRoleBuyer(model.dataModel.getPubKeyRing()); + String buyerPayoutAmount = model.btcFormatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()); + String sellerPayoutAmount = model.btcFormatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()); + String myPayoutAmount = isMyRoleBuyer ? buyerPayoutAmount : sellerPayoutAmount; + String peersPayoutAmount = isMyRoleBuyer ? sellerPayoutAmount : buyerPayoutAmount; + + acceptMediationResultPopup = new Popup<>().width(900) + .headLine(headLine) + .instruction(Res.get("portfolio.pending.mediationResult.popup.info", + myPayoutAmount, peersPayoutAmount)) + .actionButtonText(Res.get("shared.accept")) + .onAction(() -> { + model.dataModel.mediationManager.acceptMediationResult(trade, + () -> { + log.info("onAcceptMediationResult completed"); + acceptMediationResultPopup = null; + }, + errorMessage -> { + UserThread.execute(() -> { + new Popup<>().error(errorMessage).show(); + if (acceptMediationResultPopup != null) { + acceptMediationResultPopup.hide(); + acceptMediationResultPopup = null; + } + }); + }); + }) + .secondaryActionButtonText(Res.get("portfolio.pending.mediationResult.popup.openArbitration")) + .onSecondaryAction(() -> { + model.dataModel.mediationManager.rejectMediationResult(trade); + model.dataModel.onOpenDispute(); + acceptMediationResultPopup = null; + }) + .onClose(() -> { + acceptMediationResultPopup = null; + }); + + acceptMediationResultPopup.show(); + } + + protected void deactivatePaymentButtons(boolean isDisabled) { + } + private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) { - if (trade.getDisputeState() != Trade.DisputeState.DISPUTE_REQUESTED && - trade.getDisputeState() != Trade.DisputeState.DISPUTE_STARTED_BY_PEER) { + if (trade.getDisputeState() == Trade.DisputeState.NO_DISPUTE) { switch (tradePeriodState) { case FIRST_HALF: + // just for dev testing. not possible to go back in time ;-) + if (tradeStepInfo.getState() == TradeStepInfo.State.WARN_PERIOD_OVER) { + tradeStepInfo.setState(TradeStepInfo.State.WARN_HALF_PERIOD); + } else if (tradeStepInfo.getState() == TradeStepInfo.State.WARN_HALF_PERIOD) { + tradeStepInfo.setState(TradeStepInfo.State.SHOW_GET_HELP_BUTTON); + tradeStepInfo.setFistHalfOverWarnTextSupplier(this::getFistHalfOverWarnText); + } break; case SECOND_HALF: - if (!trade.isFiatReceived()) - showWarning(); - else - removeWarning(); + if (!trade.isFiatReceived()) { + if (tradeStepInfo != null) { + tradeStepInfo.setFistHalfOverWarnTextSupplier(this::getFistHalfOverWarnText); + tradeStepInfo.setState(TradeStepInfo.State.WARN_HALF_PERIOD); + } + } else { + tradeStepInfo.setState(TradeStepInfo.State.SHOW_GET_HELP_BUTTON); + } break; case TRADE_PERIOD_OVER: - onOpenForDispute(); + if (tradeStepInfo != null) { + tradeStepInfo.setFistHalfOverWarnTextSupplier(this::getPeriodOverWarnText); + tradeStepInfo.setState(TradeStepInfo.State.WARN_PERIOD_OVER); + } break; } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java index 0ba9900dcd..ab62f1d42b 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java @@ -51,8 +51,7 @@ public class BuyerStep1View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected String getWarningText() { - setWarningHeadline(); + protected String getFistHalfOverWarnText() { return Res.get("portfolio.pending.step1.warn"); } @@ -61,7 +60,7 @@ public class BuyerStep1View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected String getOpenForDisputeText() { + protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step1.openForDispute"); } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java index 9c12794ede..0ff9f97de8 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java @@ -337,8 +337,7 @@ public class BuyerStep2View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected String getWarningText() { - setWarningHeadline(); + protected String getFistHalfOverWarnText() { return Res.get("portfolio.pending.step2_buyer.warn", model.dataModel.getCurrencyCode(), model.getDateForOpenDispute()); @@ -349,26 +348,22 @@ public class BuyerStep2View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected String getOpenForDisputeText() { + protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step2_buyer.openForDispute"); } @Override protected void applyOnDisputeOpened() { - // confirmButton.setDisable(true); } /////////////////////////////////////////////////////////////////////////////////////////// // UI Handlers /////////////////////////////////////////////////////////////////////////////////////////// - @SuppressWarnings("PointlessBooleanExpression") private void onPaymentStarted() { - if (model.p2PService.isBootstrapped()) { + if (model.dataModel.isBootstrappedOrShowPopup()) { if (model.dataModel.getSellersPaymentAccountPayload() instanceof CashDepositAccountPayload) { - //noinspection UnusedAssignment String key = "confirmPaperReceiptSent"; - //noinspection ConstantConditions if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { Popup popup = new Popup<>(); popup.headLine(Res.get("portfolio.pending.step2_buyer.paperReceipt.headline")) @@ -382,8 +377,6 @@ public class BuyerStep2View extends TradeStepView { showConfirmPaymentStartedPopup(); } } else if (model.dataModel.getSellersPaymentAccountPayload() instanceof WesternUnionAccountPayload) { - //noinspection UnusedAssignment - //noinspection ConstantConditions String key = "westernUnionMTCNSent"; if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { String email = ((WesternUnionAccountPayload) model.dataModel.getSellersPaymentAccountPayload()).getEmail(); @@ -400,8 +393,6 @@ public class BuyerStep2View extends TradeStepView { showConfirmPaymentStartedPopup(); } } else if (model.dataModel.getSellersPaymentAccountPayload() instanceof MoneyGramAccountPayload) { - //noinspection UnusedAssignment - //noinspection ConstantConditions String key = "moneyGramMTCNSent"; if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { String email = ((MoneyGramAccountPayload) model.dataModel.getSellersPaymentAccountPayload()).getEmail(); @@ -418,8 +409,6 @@ public class BuyerStep2View extends TradeStepView { showConfirmPaymentStartedPopup(); } } else if (model.dataModel.getSellersPaymentAccountPayload() instanceof HalCashAccountPayload) { - //noinspection UnusedAssignment - //noinspection ConstantConditions String key = "halCashCodeInfo"; if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { String mobileNr = ((HalCashAccountPayload) model.dataModel.getSellersPaymentAccountPayload()).getMobileNr(); @@ -439,16 +428,11 @@ public class BuyerStep2View extends TradeStepView { } else { showConfirmPaymentStartedPopup(); } - } else { - new Popup<>().information(Res.get("popup.warning.notFullyConnected")).show(); } } - @SuppressWarnings("PointlessBooleanExpression") private void showConfirmPaymentStartedPopup() { - //noinspection UnusedAssignment String key = "confirmPaymentStarted"; - //noinspection ConstantConditions if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { Popup popup = new Popup<>(); popup.headLine(Res.get("portfolio.pending.step2_buyer.confirmStart.headline")) @@ -486,7 +470,6 @@ public class BuyerStep2View extends TradeStepView { }); } - @SuppressWarnings("PointlessBooleanExpression") private void showPopup() { PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); if (paymentAccountPayload != null) { @@ -502,7 +485,6 @@ public class BuyerStep2View extends TradeStepView { String paddedId = " " + id + " "; String amount = model.btcFormatter.formatVolumeWithCode(trade.getTradeVolume()); if (paymentAccountPayload instanceof AssetsAccountPayload) { - //noinspection UnusedAssignment message += Res.get("portfolio.pending.step2_buyer.altcoin", CurrencyUtil.getNameByCode(trade.getOffer().getCurrencyCode()), amount) + @@ -510,7 +492,6 @@ public class BuyerStep2View extends TradeStepView { paymentDetailsForTradePopup + ".\n\n" + copyPaste; } else if (paymentAccountPayload instanceof CashDepositAccountPayload) { - //noinspection UnusedAssignment message += Res.get("portfolio.pending.step2_buyer.cash", amount) + accountDetails + @@ -540,7 +521,6 @@ public class BuyerStep2View extends TradeStepView { copyPaste + "\n\n" + extra; } else if (paymentAccountPayload instanceof USPostalMoneyOrderAccountPayload) { - //noinspection UnusedAssignment message += Res.get("portfolio.pending.step2_buyer.postal", amount) + accountDetails + paymentDetailsForTradePopup + ".\n\n" + @@ -549,19 +529,16 @@ public class BuyerStep2View extends TradeStepView { assign + refTextWarn; } else if (paymentAccountPayload instanceof F2FAccountPayload) { - //noinspection UnusedAssignment message += Res.get("portfolio.pending.step2_buyer.f2f", amount) + accountDetails + paymentDetailsForTradePopup + "\n\n" + copyPaste; } else if (paymentAccountPayload instanceof HalCashAccountPayload) { - //noinspection UnusedAssignment message += Res.get("portfolio.pending.step2_buyer.bank", amount) + accountDetails + paymentDetailsForTradePopup + ".\n\n" + copyPaste; } else if (paymentAccountPayload instanceof FasterPaymentsAccountPayload) { - //noinspection UnusedAssignment message += Res.get("portfolio.pending.step2_buyer.bank", amount) + accountDetails + paymentDetailsForTradePopup + ".\n\n" + @@ -572,7 +549,6 @@ public class BuyerStep2View extends TradeStepView { refTextWarn + "\n\n" + fees; } else { - //noinspection UnusedAssignment message += Res.get("portfolio.pending.step2_buyer.bank", amount) + accountDetails + paymentDetailsForTradePopup + ".\n\n" + @@ -582,9 +558,7 @@ public class BuyerStep2View extends TradeStepView { refTextWarn + "\n\n" + fees; } - //noinspection ConstantConditions,UnusedAssignment String key = "startPayment" + trade.getId(); - //noinspection ConstantConditions,ConstantConditions if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { DontShowAgainLookup.dontShowAgain(key, true); new Popup<>().headLine(Res.get("popup.attention.forTradeWithId", id)) @@ -593,4 +567,9 @@ public class BuyerStep2View extends TradeStepView { } } } + + @Override + protected void deactivatePaymentButtons(boolean isDisabled) { + confirmButton.setDisable(isDisabled); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java index 457ea53bd2..115ee91e3b 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep3View.java @@ -28,7 +28,6 @@ import bisq.core.network.MessageState; import de.jensd.fx.fontawesome.AwesomeIcon; import javafx.scene.control.Label; -import javafx.scene.paint.Paint; import javafx.beans.value.ChangeListener; @@ -129,12 +128,11 @@ public class BuyerStep3View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected String getWarningText() { - setInformationHeadline(); + protected String getFistHalfOverWarnText() { String substitute = model.isBlockChainMethod() ? Res.get("portfolio.pending.step3_buyer.warn.part1a", model.dataModel.getCurrencyCode()) : Res.get("portfolio.pending.step3_buyer.warn.part1b"); - return Res.get("portfolio.pending.step3_buyer.warn.part2", substitute, model.getDateForOpenDispute()); + return Res.get("portfolio.pending.step3_buyer.warn.part2", substitute); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -142,7 +140,7 @@ public class BuyerStep3View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected String getOpenForDisputeText() { + protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step3_buyer.openForDispute"); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java index 96d94c4c66..483e26f17b 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep4View.java @@ -80,48 +80,23 @@ public class BuyerStep4View extends TradeStepView { public BuyerStep4View(PendingTradesViewModel model) { super(model); - - /* focusedPropertyListener = (ov, oldValue, newValue) -> { - if (oldValue && !newValue) - model.withdrawAddressFocusOut(withdrawAddressTextField.getText()); - };*/ } @Override public void activate() { super.activate(); - - // TODO valid. handler need improvement - //withdrawAddressTextField.focusedProperty().addListener(focusedPropertyListener); - //withdrawAddressTextField.setValidator(model.getBtcAddressValidator()); - // withdrawButton.disableProperty().bind(model.getWithdrawalButtonDisable()); - - // We need to handle both cases: Address not set and address already set (when returning from other view) - // We get address validation after focus out, so first make sure we loose focus and then set it again as hint for user to put address in - //TODO app wide focus - /* UserThread.execute(() -> { - withdrawAddressTextField.requestFocus(); - UserThread.execute(() -> { - this.requestFocus(); - UserThread.execute(() -> withdrawAddressTextField.requestFocus()); - }); - });*/ - - hideNotificationGroup(); } @Override public void deactivate() { super.deactivate(); - //withdrawAddressTextField.focusedProperty().removeListener(focusedPropertyListener); - // withdrawButton.disableProperty().unbind(); } + /////////////////////////////////////////////////////////////////////////////////////////// // Content /////////////////////////////////////////////////////////////////////////////////////////// - @SuppressWarnings("PointlessBooleanExpression") @Override protected void addContent() { gridPane.getColumnConstraints().get(1).setHgrow(Priority.SOMETIMES); @@ -164,7 +139,6 @@ public class BuyerStep4View extends TradeStepView { withdrawToExternalWalletButton.setOnAction(e -> onWithdrawal()); String key = "tradeCompleted" + trade.getId(); - //noinspection ConstantConditions if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { DontShowAgainLookup.dontShowAgain(key, true); new Notification().headLine(Res.get("notification.tradeCompleted.headline")) @@ -184,15 +158,13 @@ public class BuyerStep4View extends TradeStepView { useSavingsWalletButton.getStyleClass().remove("action-button"); withdrawToExternalWalletButton.setOnAction(e -> { - if (model.dataModel.isReadyForTxBroadcast()) + if (model.dataModel.isReadyForTxBroadcast()) { reviewWithdrawal(); - else - model.dataModel.showNotReadyForTxBroadcastPopups(); + } }); } - @SuppressWarnings("PointlessBooleanExpression") private void reviewWithdrawal() { Coin amount = trade.getPayoutAmount(); BtcWalletService walletService = model.dataModel.btcWalletService; @@ -205,7 +177,6 @@ public class BuyerStep4View extends TradeStepView { try { Transaction feeEstimationTransaction = walletService.getFeeEstimationTransaction(fromAddresses, toAddresses, amount, AddressEntry.Context.TRADE_PAYOUT); Coin fee = feeEstimationTransaction.getFee(); - //noinspection UnusedAssignment Coin receiverAmount = amount.subtract(fee); if (balance.isZero()) { new Popup<>().warning(Res.get("portfolio.pending.step5_buyer.alreadyWithdrawn")).show(); @@ -273,7 +244,12 @@ public class BuyerStep4View extends TradeStepView { doWithdrawRequest(toAddress, amount, fee, null, resultHandler, faultHandler); } - private void doWithdrawRequest(String toAddress, Coin amount, Coin fee, KeyParameter aesKey, ResultHandler resultHandler, FaultHandler faultHandler) { + private void doWithdrawRequest(String toAddress, + Coin amount, + Coin fee, + KeyParameter aesKey, + ResultHandler resultHandler, + FaultHandler faultHandler) { useSavingsWalletButton.setDisable(true); withdrawToExternalWalletButton.setDisable(true); model.dataModel.onWithdrawRequest(toAddress, @@ -284,7 +260,6 @@ public class BuyerStep4View extends TradeStepView { faultHandler); } - @SuppressWarnings("PointlessBooleanExpression") private void handleTradeCompleted() { useSavingsWalletButton.setDisable(true); withdrawToExternalWalletButton.setDisable(true); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java index 75be150a72..9e50fc4fb5 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep1View.java @@ -51,8 +51,7 @@ public class SellerStep1View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected String getWarningText() { - setWarningHeadline(); + protected String getFistHalfOverWarnText() { return Res.get("portfolio.pending.step1.warn"); } @@ -61,7 +60,7 @@ public class SellerStep1View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected String getOpenForDisputeText() { + protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step1.openForDispute"); } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java index 91db9d87bc..2b85a5bfe7 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep2View.java @@ -70,8 +70,7 @@ public class SellerStep2View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected String getWarningText() { - setInformationHeadline(); + protected String getFistHalfOverWarnText() { return Res.get("portfolio.pending.step2_seller.warn", model.dataModel.getCurrencyCode(), model.getDateForOpenDispute()); @@ -83,7 +82,7 @@ public class SellerStep2View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected String getOpenForDisputeText() { + protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step2_seller.openForDispute"); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index 846e6d3374..e9514dccbb 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -246,12 +246,11 @@ public class SellerStep3View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected String getWarningText() { - setWarningHeadline(); + protected String getFistHalfOverWarnText() { String substitute = model.isBlockChainMethod() ? Res.get("portfolio.pending.step3_seller.warn.part1a", model.dataModel.getCurrencyCode()) : Res.get("portfolio.pending.step3_seller.warn.part1b"); - return Res.get("portfolio.pending.step3_seller.warn.part2", substitute, model.getDateForOpenDispute()); + return Res.get("portfolio.pending.step3_seller.warn.part2", substitute); } @@ -261,27 +260,23 @@ public class SellerStep3View extends TradeStepView { /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected String getOpenForDisputeText() { + protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step3_seller.openForDispute"); } @Override protected void applyOnDisputeOpened() { - // confirmButton.setDisable(true); } //////////////////////////////////////////////////////////////////////////////////////// // UI Handlers /////////////////////////////////////////////////////////////////////////////////////////// - @SuppressWarnings("PointlessBooleanExpression") private void onPaymentReceived() { // The confirmPaymentReceived call will trigger the trade protocol to do the payout tx. We want to be sure that we // are well connected to the Bitcoin network before triggering the broadcast. if (model.dataModel.isReadyForTxBroadcast()) { - //noinspection UnusedAssignment String key = "confirmPaymentReceived"; - //noinspection ConstantConditions if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); String message = Res.get("portfolio.pending.step3_seller.onPaymentReceived.part1", CurrencyUtil.getNameByCode(model.dataModel.getCurrencyCode())); @@ -309,15 +304,11 @@ public class SellerStep3View extends TradeStepView { } else { confirmPaymentReceived(); } - } else { - model.dataModel.showNotReadyForTxBroadcastPopups(); } } - @SuppressWarnings("PointlessBooleanExpression") private void showPopup() { PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); - //noinspection UnusedAssignment String key = "confirmPayment" + trade.getId(); String message = ""; String tradeVolumeWithCode = model.btcFormatter.formatVolumeWithCode(trade.getTradeVolume()); @@ -329,7 +320,6 @@ public class SellerStep3View extends TradeStepView { String explorerOrWalletString = trade.getOffer().getCurrencyCode().equals("XMR") ? Res.get("portfolio.pending.step3_seller.altcoin.wallet", currencyName) : Res.get("portfolio.pending.step3_seller.altcoin.explorer", currencyName); - //noinspection UnusedAssignment message = Res.get("portfolio.pending.step3_seller.altcoin", part1, explorerOrWalletString, address, tradeVolumeWithCode, currencyName); } else { if (paymentAccountPayload instanceof USPostalMoneyOrderAccountPayload) { @@ -354,11 +344,9 @@ public class SellerStep3View extends TradeStepView { Optional optionalHolderName = getOptionalHolderName(); if (optionalHolderName.isPresent()) { - //noinspection UnusedAssignment message = message + Res.get("portfolio.pending.step3_seller.bankCheck", optionalHolderName.get(), part); } } - //noinspection ConstantConditions if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { DontShowAgainLookup.dontShowAgain(key, true); new Popup<>().headLine(Res.get("popup.attention.forTradeWithId", id)) @@ -403,6 +391,11 @@ public class SellerStep3View extends TradeStepView { return Optional.empty(); } } + + @Override + protected void deactivatePaymentButtons(boolean isDisabled) { + confirmButton.setDisable(isDisabled); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/presentation/MarketPricePresentation.java b/desktop/src/main/java/bisq/desktop/main/presentation/MarketPricePresentation.java index 870304ecf6..f28757ccf2 100644 --- a/desktop/src/main/java/bisq/desktop/main/presentation/MarketPricePresentation.java +++ b/desktop/src/main/java/bisq/desktop/main/presentation/MarketPricePresentation.java @@ -19,7 +19,7 @@ package bisq.desktop.main.presentation; import bisq.desktop.components.BalanceWithConfirmationTextField; import bisq.desktop.components.TxIdTextField; -import bisq.desktop.main.PriceFeedComboBoxItem; +import bisq.desktop.main.shared.PriceFeedComboBoxItem; import bisq.desktop.util.GUIUtil; import bisq.core.btc.wallet.BtcWalletService; @@ -87,7 +87,6 @@ public class MarketPricePresentation { // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - @SuppressWarnings("WeakerAccess") @Inject public MarketPricePresentation(BtcWalletService btcWalletService, PriceFeedService priceFeedService, diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesViewModel.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesViewModel.java index db7febe092..2f794b7d84 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesViewModel.java @@ -20,8 +20,8 @@ package bisq.desktop.main.settings.preferences; import bisq.desktop.common.model.ActivatableViewModel; -import bisq.core.arbitration.ArbitratorManager; import bisq.core.locale.LanguageUtil; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.user.Preferences; import com.google.inject.Inject; @@ -40,11 +40,11 @@ public class PreferencesViewModel extends ActivatableViewModel { } boolean needsArbitrationLanguageWarning() { - return !arbitratorManager.isArbitratorAvailableForLanguage(preferences.getUserLanguage()); + return !arbitratorManager.isAgentAvailableForLanguage(preferences.getUserLanguage()); } String getArbitrationLanguages() { - return arbitratorManager.getArbitratorsObservableMap().values().stream() + return arbitratorManager.getObservableMap().values().stream() .flatMap(arbitrator -> arbitrator.getLanguageCodes().stream()) .distinct() .map(languageCode -> LanguageUtil.getDisplayName(languageCode)) diff --git a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java b/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java similarity index 80% rename from desktop/src/main/java/bisq/desktop/main/Chat/Chat.java rename to desktop/src/main/java/bisq/desktop/main/shared/ChatView.java index d6a1038368..356dbddcdf 100644 --- a/desktop/src/main/java/bisq/desktop/main/Chat/Chat.java +++ b/desktop/src/main/java/bisq/desktop/main/shared/ChatView.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main.Chat; +package bisq.desktop.main.shared; import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; @@ -26,22 +26,17 @@ import bisq.desktop.components.TextFieldWithIcon; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.GUIUtil; -import bisq.core.arbitration.Attachment; -import bisq.core.arbitration.messages.DisputeCommunicationMessage; -import bisq.core.chat.ChatManager; -import bisq.core.chat.ChatSession; import bisq.core.locale.Res; -import bisq.core.trade.TradeChatSession; +import bisq.core.support.SupportManager; +import bisq.core.support.SupportSession; +import bisq.core.support.dispute.Attachment; +import bisq.core.support.messages.ChatMessage; import bisq.core.util.BSFormatter; -import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.P2PService; -import bisq.network.p2p.SendMailboxMessageListener; import bisq.network.p2p.network.Connection; import bisq.common.Timer; import bisq.common.UserThread; -import bisq.common.crypto.PubKeyRing; import bisq.common.util.Utilities; import com.google.common.io.ByteStreams; @@ -95,6 +90,7 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; @@ -105,13 +101,13 @@ import lombok.Setter; import javax.annotation.Nullable; -public class Chat extends AnchorPane { +public class ChatView extends AnchorPane { public static final Logger log = LoggerFactory.getLogger(TextFieldWithIcon.class); // UI private TextArea inputTextArea; private Button sendButton; - private ListView messageListView; + private ListView messageListView; private Label sendMsgInfoLabel; private BusyAnimation sendMsgBusyAnimation; private TableGroupHeadline tableGroupHeadline; @@ -128,10 +124,9 @@ public class Chat extends AnchorPane { boolean displayHeader; // Communication stuff, to be renamed to something more generic - private final P2PService p2PService; - private DisputeCommunicationMessage disputeCommunicationMessage; - private ObservableList disputeCommunicationMessages; - private ListChangeListener disputeDirectMessageListListener; + private ChatMessage chatMessage; + private ObservableList chatMessages; + private ListChangeListener disputeDirectMessageListListener; private Subscription inputTextAreaTextSubscription; private final List tempAttachments = new ArrayList<>(); private ChangeListener storedInMailboxPropertyListener, arrivedPropertyListener; @@ -139,12 +134,12 @@ public class Chat extends AnchorPane { protected final BSFormatter formatter; private EventHandler keyEventEventHandler; - private ChatManager chatManager; + private SupportManager supportManager; + private Optional optionalSupportSession = Optional.empty(); - public Chat(ChatManager chatManager, BSFormatter formatter) { - this.chatManager = chatManager; + public ChatView(SupportManager supportManager, BSFormatter formatter) { + this.supportManager = supportManager; this.formatter = formatter; - this.p2PService = chatManager.getP2PService(); allowAttachments = true; displayHeader = true; } @@ -154,8 +149,11 @@ public class Chat extends AnchorPane { keyEventEventHandler = event -> { if (Utilities.isAltOrCtrlPressed(KeyCode.ENTER, event)) { - if (chatManager.getChatSession().chatIsOpen() && inputTextArea.isFocused()) - onTrySendMessage(); + optionalSupportSession.ifPresent(supportSession -> { + if (supportSession.chatIsOpen() && inputTextArea.isFocused()) { + onTrySendMessage(); + } + }); } }; } @@ -169,10 +167,15 @@ public class Chat extends AnchorPane { removeListenersOnSessionChange(); } - public void display(ChatSession chatSession, @Nullable Button extraButton, + public void display(SupportSession supportSession, ReadOnlyDoubleProperty widthProperty) { + display(supportSession, null, widthProperty); + } + + public void display(SupportSession supportSession, + @Nullable Button extraButton, ReadOnlyDoubleProperty widthProperty) { + optionalSupportSession = Optional.of(supportSession); removeListenersOnSessionChange(); - chatManager.setChatSession(chatSession); this.getChildren().clear(); this.extraButton = extraButton; this.widthProperty = widthProperty; @@ -185,8 +188,8 @@ public class Chat extends AnchorPane { AnchorPane.setBottomAnchor(tableGroupHeadline, 0d); AnchorPane.setLeftAnchor(tableGroupHeadline, 0d); - disputeCommunicationMessages = chatSession.getDisputeCommunicationMessages(); - SortedList sortedList = new SortedList<>(disputeCommunicationMessages); + chatMessages = supportSession.getObservableChatMessageList(); + SortedList sortedList = new SortedList<>(chatMessages); sortedList.setComparator(Comparator.comparing(o -> new Date(o.getDate()))); messageListView = new ListView<>(sortedList); messageListView.setId("message-list-view"); @@ -201,7 +204,8 @@ public class Chat extends AnchorPane { inputTextArea = new BisqTextArea(); inputTextArea.setPrefHeight(70); inputTextArea.setWrapText(true); - if (chatSession instanceof TradeChatSession || chatSession.isClient()) { + + if (!supportSession.isDisputeAgent()) { inputTextArea.setPromptText(Res.get("support.input.prompt")); } @@ -223,7 +227,7 @@ public class Chat extends AnchorPane { if (displayHeader) this.getChildren().add(tableGroupHeadline); - if (chatSession.chatIsOpen()) { + if (supportSession.chatIsOpen()) { HBox buttonBox = new HBox(); buttonBox.setSpacing(10); if (allowAttachments) @@ -257,25 +261,25 @@ public class Chat extends AnchorPane { messageListView.setCellFactory(new Callback<>() { @Override - public ListCell call(ListView list) { + public ListCell call(ListView list) { return new ListCell<>() { ChangeListener sendMsgBusyAnimationListener; - final Pane bg = new Pane(); - final ImageView arrow = new ImageView(); - final Label headerLabel = new AutoTooltipLabel(); - final Label messageLabel = new AutoTooltipLabel(); - final Label copyIcon = new Label(); - final HBox attachmentsBox = new HBox(); - final AnchorPane messageAnchorPane = new AnchorPane(); - final Label statusIcon = new Label(); - final Label statusInfoLabel = new Label(); - final HBox statusHBox = new HBox(); - final double arrowWidth = 15d; - final double attachmentsBoxHeight = 20d; - final double border = 10d; - final double bottomBorder = 25d; - final double padding = border + 10d; - final double msgLabelPaddingRight = padding + 20d; + Pane bg = new Pane(); + ImageView arrow = new ImageView(); + Label headerLabel = new AutoTooltipLabel(); + Label messageLabel = new AutoTooltipLabel(); + Label copyIcon = new Label(); + HBox attachmentsBox = new HBox(); + AnchorPane messageAnchorPane = new AnchorPane(); + Label statusIcon = new Label(); + Label statusInfoLabel = new Label(); + HBox statusHBox = new HBox(); + double arrowWidth = 15d; + double attachmentsBoxHeight = 20d; + double border = 10d; + double bottomBorder = 25d; + double padding = border + 10d; + double msgLabelPaddingRight = padding + 20d; { bg.setMinHeight(30); @@ -292,7 +296,7 @@ public class Chat extends AnchorPane { } @Override - public void updateItem(final DisputeCommunicationMessage message, boolean empty) { + public void updateItem(ChatMessage message, boolean empty) { super.updateItem(message, empty); if (message != null && !empty) { copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(messageLabel.getText())); @@ -316,7 +320,7 @@ public class Chat extends AnchorPane { AnchorPane.setBottomAnchor(attachmentsBox, bottomBorder + 10); boolean senderIsTrader = message.isSenderIsTrader(); - boolean isMyMsg = chatSession.isClient() == senderIsTrader; + boolean isMyMsg = supportSession.isClient() == senderIsTrader; arrow.setVisible(!message.isSystemMessage()); arrow.setManaged(!message.isSystemMessage()); @@ -339,7 +343,7 @@ public class Chat extends AnchorPane { bg.setId("message-bubble-blue"); messageLabel.getStyleClass().add("my-message"); copyIcon.getStyleClass().add("my-message"); - if (chatSession.isClient()) + if (supportSession.isClient()) arrow.setId("bubble_arrow_blue_left"); else arrow.setId("bubble_arrow_blue_right"); @@ -360,7 +364,7 @@ public class Chat extends AnchorPane { bg.setId("message-bubble-grey"); messageLabel.getStyleClass().add("message"); copyIcon.getStyleClass().add("message"); - if (chatSession.isClient()) + if (supportSession.isClient()) arrow.setId("bubble_arrow_grey_right"); else arrow.setId("bubble_arrow_grey_left"); @@ -419,7 +423,7 @@ public class Chat extends AnchorPane { getStyleClass().add("message"); }}); message.getAttachments().forEach(attachment -> { - final Label icon = new Label(); + Label icon = new Label(); setPadding(new Insets(0, 0, 3, 0)); if (isMyMsg) icon.getStyleClass().add("attachment-icon"); @@ -462,7 +466,7 @@ public class Chat extends AnchorPane { } } - private void updateMsgState(DisputeCommunicationMessage message) { + private void updateMsgState(ChatMessage message) { boolean visible; AwesomeIcon icon = null; String text = null; @@ -517,7 +521,7 @@ public class Chat extends AnchorPane { /////////////////////////////////////////////////////////////////////////////////////////// private void onTrySendMessage() { - if (p2PService.isBootstrapped()) { + if (supportManager.isBootstrapped()) { String text = inputTextArea.getText(); if (!text.isEmpty()) { if (text.length() < 5_000) { @@ -592,13 +596,13 @@ public class Chat extends AnchorPane { } private void onSendMessage(String inputText) { - if (disputeCommunicationMessage != null) { - disputeCommunicationMessage.arrivedProperty().removeListener(arrivedPropertyListener); - disputeCommunicationMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener); - disputeCommunicationMessage.sendMessageErrorProperty().removeListener(sendMessageErrorPropertyListener); + if (chatMessage != null) { + chatMessage.arrivedProperty().removeListener(arrivedPropertyListener); + chatMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener); + chatMessage.sendMessageErrorProperty().removeListener(sendMessageErrorPropertyListener); } - disputeCommunicationMessage = sendDisputeDirectMessage(inputText, new ArrayList<>(tempAttachments)); + chatMessage = sendDisputeDirectMessage(inputText, new ArrayList<>(tempAttachments)); tempAttachments.clear(); scrollToBottom(); @@ -634,65 +638,27 @@ public class Chat extends AnchorPane { hideSendMsgInfo(timer); } }; - if (disputeCommunicationMessage != null) { - disputeCommunicationMessage.arrivedProperty().addListener(arrivedPropertyListener); - disputeCommunicationMessage.storedInMailboxProperty().addListener(storedInMailboxPropertyListener); - disputeCommunicationMessage.sendMessageErrorProperty().addListener(sendMessageErrorPropertyListener); + if (chatMessage != null) { + chatMessage.arrivedProperty().addListener(arrivedPropertyListener); + chatMessage.storedInMailboxProperty().addListener(storedInMailboxPropertyListener); + chatMessage.sendMessageErrorProperty().addListener(sendMessageErrorPropertyListener); } } - private DisputeCommunicationMessage sendDisputeDirectMessage(String text, ArrayList attachments) { - DisputeCommunicationMessage message = new DisputeCommunicationMessage( - chatManager.getChatSession().getType(), - chatManager.getChatSession().getTradeId(), - chatManager.getChatSession().getClientPubKeyRing().hashCode(), - chatManager.getChatSession().isClient(), - text, - p2PService.getAddress() - ); - - message.addAllAttachments(attachments); - NodeAddress peersNodeAddress = chatManager.getChatSession().getPeerNodeAddress(message); - PubKeyRing receiverPubKeyRing = chatManager.getChatSession().getPeerPubKeyRing(message); - - chatManager.getChatSession().addDisputeCommunicationMessage(message); - - if (receiverPubKeyRing != null) { - log.info("Send {} to peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - - p2PService.sendEncryptedMailboxMessage(peersNodeAddress, - receiverPubKeyRing, - message, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - message.setArrived(true); - chatManager.getChatSession().persist(); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); - message.setStoredInMailbox(true); - chatManager.getChatSession().persist(); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", - message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage); - message.setSendMessageError(errorMessage); - chatManager.getChatSession().persist(); - } - } + private ChatMessage sendDisputeDirectMessage(String text, ArrayList attachments) { + return optionalSupportSession.map(supportSession -> { + ChatMessage message = new ChatMessage( + supportManager.getSupportType(), + supportSession.getTradeId(), + supportSession.getClientPubKeyRing().hashCode(), + supportSession.isClient(), + text, + supportManager.getMyAddress(), + attachments ); - } - - return message; + supportManager.addAndPersistChatMessage(message); + return supportManager.sendChatMessage(message); + }).orElse(null); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -734,20 +700,20 @@ public class Chat extends AnchorPane { tableGroupHeadline.prefWidthProperty().bind(widthProperty); messageListView.prefWidthProperty().bind(widthProperty); this.prefWidthProperty().bind(widthProperty); - disputeCommunicationMessages.addListener(disputeDirectMessageListListener); + chatMessages.addListener(disputeDirectMessageListListener); inputTextAreaTextSubscription = EasyBind.subscribe(inputTextArea.textProperty(), t -> sendButton.setDisable(t.isEmpty())); } } private void removeListenersOnSessionChange() { - if (disputeCommunicationMessages != null && disputeDirectMessageListListener != null) - disputeCommunicationMessages.removeListener(disputeDirectMessageListListener); + if (chatMessages != null && disputeDirectMessageListListener != null) + chatMessages.removeListener(disputeDirectMessageListListener); - if (disputeCommunicationMessage != null) { + if (chatMessage != null) { if (arrivedPropertyListener != null) - disputeCommunicationMessage.arrivedProperty().removeListener(arrivedPropertyListener); + chatMessage.arrivedProperty().removeListener(arrivedPropertyListener); if (storedInMailboxPropertyListener != null) - disputeCommunicationMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener); + chatMessage.storedInMailboxProperty().removeListener(storedInMailboxPropertyListener); } if (messageListView != null) diff --git a/desktop/src/main/java/bisq/desktop/main/PriceFeedComboBoxItem.java b/desktop/src/main/java/bisq/desktop/main/shared/PriceFeedComboBoxItem.java similarity index 97% rename from desktop/src/main/java/bisq/desktop/main/PriceFeedComboBoxItem.java rename to desktop/src/main/java/bisq/desktop/main/shared/PriceFeedComboBoxItem.java index 36b34586b3..60b717e364 100644 --- a/desktop/src/main/java/bisq/desktop/main/PriceFeedComboBoxItem.java +++ b/desktop/src/main/java/bisq/desktop/main/shared/PriceFeedComboBoxItem.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main; +package bisq.desktop.main.shared; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/DisputesView.fxml b/desktop/src/main/java/bisq/desktop/main/support/SupportView.fxml similarity index 83% rename from desktop/src/main/java/bisq/desktop/main/disputes/DisputesView.fxml rename to desktop/src/main/java/bisq/desktop/main/support/SupportView.fxml index b916b4519e..7c5fc259ac 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/DisputesView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/support/SupportView.fxml @@ -20,11 +20,12 @@ - - + + diff --git a/desktop/src/main/java/bisq/desktop/main/support/SupportView.java b/desktop/src/main/java/bisq/desktop/main/support/SupportView.java new file mode 100644 index 0000000000..b7fc867bcd --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/SupportView.java @@ -0,0 +1,227 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.main.support; + +import bisq.desktop.Navigation; +import bisq.desktop.common.model.Activatable; +import bisq.desktop.common.view.ActivatableViewAndModel; +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.main.MainView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.support.dispute.agent.arbitration.ArbitratorView; +import bisq.desktop.main.support.dispute.agent.mediation.MediatorView; +import bisq.desktop.main.support.dispute.client.arbitration.ArbitrationClientView; +import bisq.desktop.main.support.dispute.client.mediation.MediationClientView; + +import bisq.core.locale.Res; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.mediation.mediator.Mediator; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.DevEnv; +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.PubKeyRing; + +import javax.inject.Inject; + +import javafx.fxml.FXML; + +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; + +import javafx.beans.value.ChangeListener; + +import javafx.collections.MapChangeListener; + +@FxmlView +public class SupportView extends ActivatableViewAndModel { + + @FXML + Tab tradersArbitrationDisputesTab, tradersMediationDisputesTab; + + private Tab arbitratorTab, mediatorTab; + + private final Navigation navigation; + private final ArbitratorManager arbitratorManager; + private final MediatorManager mediatorManager; + private final ArbitrationManager arbitrationManager; + private final MediationManager mediationManager; + private final KeyRing keyRing; + + private Navigation.Listener navigationListener; + private ChangeListener tabChangeListener; + private Tab currentTab; + private final ViewLoader viewLoader; + private MapChangeListener arbitratorMapChangeListener; + private MapChangeListener mediatorMapChangeListener; + + @Inject + public SupportView(CachingViewLoader viewLoader, + Navigation navigation, + ArbitratorManager arbitratorManager, + MediatorManager mediatorManager, + ArbitrationManager arbitrationManager, + MediationManager mediationManager, + KeyRing keyRing) { + this.viewLoader = viewLoader; + this.navigation = navigation; + this.arbitratorManager = arbitratorManager; + this.mediatorManager = mediatorManager; + this.arbitrationManager = arbitrationManager; + this.mediationManager = mediationManager; + this.keyRing = keyRing; + } + + @Override + public void initialize() { + // has to be called before loadView + updateAgentTabs(); + + tradersArbitrationDisputesTab.setText(Res.get("support.tab.arbitration.support").toUpperCase()); + tradersMediationDisputesTab.setText(Res.get("support.tab.mediation.support").toUpperCase()); + navigationListener = viewPath -> { + if (viewPath.size() == 3 && viewPath.indexOf(SupportView.class) == 1) + loadView(viewPath.tip()); + }; + + tabChangeListener = (ov, oldValue, newValue) -> { + if (newValue == tradersArbitrationDisputesTab) + navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class); + else if (newValue == tradersMediationDisputesTab) + navigation.navigateTo(MainView.class, SupportView.class, MediationClientView.class); + else if (newValue == arbitratorTab) + navigation.navigateTo(MainView.class, SupportView.class, ArbitratorView.class); + else if (newValue == mediatorTab) + navigation.navigateTo(MainView.class, SupportView.class, MediatorView.class); + }; + + arbitratorMapChangeListener = change -> updateAgentTabs(); + mediatorMapChangeListener = change -> updateAgentTabs(); + + } + + private void updateAgentTabs() { + PubKeyRing myPubKeyRing = keyRing.getPubKeyRing(); + boolean isActiveArbitrator = arbitratorManager.getObservableMap().values().stream() + .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); + boolean isActiveMediator = mediatorManager.getObservableMap().values().stream() + .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); + + if (arbitratorTab == null) { + // In case a arbitrator has become inactive he still might get disputes from pending trades + boolean hasDisputesAsArbitrator = arbitrationManager.getDisputesAsObservableList().stream() + .anyMatch(d -> d.getAgentPubKeyRing().equals(myPubKeyRing)); + if (isActiveArbitrator || hasDisputesAsArbitrator) { + arbitratorTab = new Tab(); + arbitratorTab.setClosable(false); + root.getTabs().add(arbitratorTab); + } + } + if (mediatorTab == null) { + // In case a mediator has become inactive he still might get disputes from pending trades + boolean hasDisputesAsMediator = mediationManager.getDisputesAsObservableList().stream() + .anyMatch(d -> d.getAgentPubKeyRing().equals(myPubKeyRing)); + if (isActiveMediator || hasDisputesAsMediator) { + mediatorTab = new Tab(); + mediatorTab.setClosable(false); + root.getTabs().add(mediatorTab); + } + } + + // We might get that method called before we have the map is filled in the arbitratorManager + if (arbitratorTab != null) { + arbitratorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.arbitrator2")).toUpperCase()); + } + if (mediatorTab != null) { + mediatorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.mediator")).toUpperCase()); + } + } + + @Override + protected void activate() { + arbitratorManager.updateMap(); + arbitratorManager.getObservableMap().addListener(arbitratorMapChangeListener); + + mediatorManager.updateMap(); + mediatorManager.getObservableMap().addListener(mediatorMapChangeListener); + + updateAgentTabs(); + + root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); + navigation.addListener(navigationListener); + + if (root.getSelectionModel().getSelectedItem() == tradersMediationDisputesTab) { + navigation.navigateTo(MainView.class, SupportView.class, MediationClientView.class); + } else if (root.getSelectionModel().getSelectedItem() == tradersArbitrationDisputesTab) { + navigation.navigateTo(MainView.class, SupportView.class, ArbitrationClientView.class); + } else if (arbitratorTab != null) { + navigation.navigateTo(MainView.class, SupportView.class, ArbitratorView.class); + } else if (mediatorTab != null) { + navigation.navigateTo(MainView.class, SupportView.class, MediatorView.class); + } + + String key = "supportInfo"; + if (!DevEnv.isDevMode()) + new Popup<>().backgroundInfo(Res.get("support.backgroundInfo")) + .width(900) + .dontShowAgainId(key) + .show(); + } + + @Override + protected void deactivate() { + arbitratorManager.getObservableMap().removeListener(arbitratorMapChangeListener); + mediatorManager.getObservableMap().removeListener(mediatorMapChangeListener); + root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); + navigation.removeListener(navigationListener); + currentTab = null; + } + + private void loadView(Class viewClass) { + // we want to get activate/deactivate called, so we remove the old view on tab change + if (currentTab != null) + currentTab.setContent(null); + + View view = viewLoader.load(viewClass); + + if (view instanceof MediationClientView) { + currentTab = tradersMediationDisputesTab; + } else if (view instanceof ArbitrationClientView) { + currentTab = tradersArbitrationDisputesTab; + } else if (view instanceof ArbitratorView) { + currentTab = arbitratorTab; + } else if (view instanceof MediatorView) { + currentTab = mediatorTab; + } else { + currentTab = null; + } + + if (currentTab != null) { + currentTab.setContent(view.getRoot()); + root.getSelectionModel().select(currentTab); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java similarity index 92% rename from desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java rename to desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java index 13783cfd5f..95d919049d 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java @@ -15,7 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main.disputes.trader; +package bisq.desktop.main.support.dispute; import bisq.desktop.common.view.ActivatableView; import bisq.desktop.common.view.FxmlView; @@ -24,39 +24,35 @@ import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.AutoTooltipTableColumn; import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.InputTextField; -import bisq.desktop.main.Chat.Chat; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; import bisq.desktop.main.overlays.windows.SendPrivateNotificationWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; +import bisq.desktop.main.shared.ChatView; import bisq.desktop.util.GUIUtil; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; -import bisq.core.app.AppOptionKeys; -import bisq.core.arbitration.Dispute; -import bisq.core.arbitration.DisputeChatSession; -import bisq.core.arbitration.DisputeManager; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeSession; import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.util.BSFormatter; import bisq.network.p2p.NodeAddress; -import bisq.network.p2p.P2PService; import bisq.common.app.Version; import bisq.common.crypto.KeyRing; import bisq.common.crypto.PubKeyRing; import bisq.common.util.Utilities; -import com.google.inject.name.Named; - -import javax.inject.Inject; - import com.google.common.collect.Lists; import javafx.scene.Scene; @@ -102,11 +98,10 @@ import java.util.Optional; import lombok.Getter; -// will be probably only used for arbitration communication, will be renamed and the icon changed @FxmlView -public class TraderDisputeView extends ActivatableView { +public abstract class DisputeView extends ActivatableView { - private final DisputeManager disputeManager; + protected final DisputeManager> disputeManager; protected final KeyRing keyRing; private final TradeManager tradeManager; protected final BSFormatter formatter; @@ -114,7 +109,6 @@ public class TraderDisputeView extends ActivatableView { private final PrivateNotificationManager privateNotificationManager; private final ContractWindow contractWindow; private final TradeDetailsWindow tradeDetailsWindow; - private final P2PService p2PService; private final AccountAgeWitnessService accountAgeWitnessService; private final boolean useDevPrivilegeKeys; @@ -123,9 +117,9 @@ public class TraderDisputeView extends ActivatableView { private SortedList sortedList; @Getter - private Dispute selectedDispute; + protected Dispute selectedDispute; - private Chat disputeChat; + protected ChatView chatView; private ChangeListener selectedDisputeClosedPropertyListener; private Subscription selectedDisputeSubscription; @@ -141,18 +135,16 @@ public class TraderDisputeView extends ActivatableView { // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// - @Inject - public TraderDisputeView(DisputeManager disputeManager, - KeyRing keyRing, - TradeManager tradeManager, - BSFormatter formatter, - DisputeSummaryWindow disputeSummaryWindow, - PrivateNotificationManager privateNotificationManager, - ContractWindow contractWindow, - TradeDetailsWindow tradeDetailsWindow, - P2PService p2PService, - AccountAgeWitnessService accountAgeWitnessService, - @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + public DisputeView(DisputeManager> disputeManager, + KeyRing keyRing, + TradeManager tradeManager, + BSFormatter formatter, + DisputeSummaryWindow disputeSummaryWindow, + PrivateNotificationManager privateNotificationManager, + ContractWindow contractWindow, + TradeDetailsWindow tradeDetailsWindow, + AccountAgeWitnessService accountAgeWitnessService, + boolean useDevPrivilegeKeys) { this.disputeManager = disputeManager; this.keyRing = keyRing; this.tradeManager = tradeManager; @@ -161,7 +153,6 @@ public class TraderDisputeView extends ActivatableView { this.privateNotificationManager = privateNotificationManager; this.contractWindow = contractWindow; this.tradeDetailsWindow = tradeDetailsWindow; - this.p2PService = p2PService; this.accountAgeWitnessService = accountAgeWitnessService; this.useDevPrivilegeKeys = useDevPrivilegeKeys; } @@ -229,7 +220,7 @@ public class TraderDisputeView extends ActivatableView { dateColumn.setSortType(TableColumn.SortType.DESCENDING); tableView.getSortOrder().add(dateColumn); - selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> disputeChat.setInputBoxVisible(!newValue); + selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> chatView.setInputBoxVisible(!newValue); keyEventEventHandler = event -> { if (Utilities.isAltOrCtrlPressed(KeyCode.L, event)) { @@ -276,7 +267,7 @@ public class TraderDisputeView extends ActivatableView { .append(dispute.getTraderId()) .append("\n*******************************************************************************************\n") .append("\n"); - dispute.getDisputeCommunicationMessages().forEach(m -> { + dispute.getChatMessages().forEach(m -> { String role = m.isSenderIsTrader() ? ">> Trader's msg: " : "<< Arbitrator's msg: "; stringBuilder.append(role) .append(m.getMessage()) @@ -317,14 +308,13 @@ public class TraderDisputeView extends ActivatableView { } }; - disputeChat = new Chat(disputeManager.getChatManager(), formatter); - disputeChat.initialize(); + chatView = new ChatView(disputeManager, formatter); + chatView.initialize(); } @Override protected void activate() { filterTextField.textProperty().addListener(filterTextFieldListener); - disputeManager.cleanupDisputes(); filteredList = new FilteredList<>(disputeManager.getDisputesAsObservableList()); applyFilteredListPredicate(filterTextField.getText()); @@ -340,9 +330,9 @@ public class TraderDisputeView extends ActivatableView { if (selectedItem != null) tableView.getSelectionModel().select(selectedItem); - if (disputeChat != null) { - disputeChat.activate(); - disputeChat.scrollToBottom(); + if (chatView != null) { + chatView.activate(); + chatView.scrollToBottom(); } scene = root.getScene(); @@ -409,13 +399,17 @@ public class TraderDisputeView extends ActivatableView { if (scene != null) scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); - if (disputeChat != null) - disputeChat.deactivate(); + if (chatView != null) + chatView.deactivate(); } + protected abstract SupportType getType(); + + protected abstract DisputeSession getConcreteDisputeChatSession(Dispute dispute); + protected void applyFilteredListPredicate(String filterString) { // If in trader view we must not display arbitrators own disputes as trader (must not happen anyway) - filteredList.setPredicate(dispute -> !dispute.getArbitratorPubKeyRing().equals(keyRing.getPubKeyRing())); + filteredList.setPredicate(dispute -> !dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())); } @@ -427,19 +421,19 @@ public class TraderDisputeView extends ActivatableView { contractWindow.show(dispute); } - private void removeListenersOnSelectDispute() { + protected void removeListenersOnSelectDispute() { if (selectedDispute != null) { if (selectedDisputeClosedPropertyListener != null) selectedDispute.isClosedProperty().removeListener(selectedDisputeClosedPropertyListener); } } - private void addListenersOnSelectDispute() { + protected void addListenersOnSelectDispute() { if (selectedDispute != null) selectedDispute.isClosedProperty().addListener(selectedDisputeClosedPropertyListener); } - private void onSelectDispute(Dispute dispute) { + protected void onSelectDispute(Dispute dispute) { removeListenersOnSelectDispute(); if (dispute == null) { if (root.getChildren().size() > 2) @@ -448,29 +442,24 @@ public class TraderDisputeView extends ActivatableView { selectedDispute = null; } else if (selectedDispute != dispute) { this.selectedDispute = dispute; - if (disputeChat != null) { - Button closeDisputeButton = null; - if (!dispute.isClosed() && !disputeManager.isTrader(dispute)) { - closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); - closeDisputeButton.setOnAction(e -> onCloseDispute(getSelectedDispute())); - } - disputeChat.display(new DisputeChatSession(dispute, disputeManager), closeDisputeButton, - root.widthProperty() - ); + if (chatView != null) { + handleOnSelectDispute(dispute); } if (root.getChildren().size() > 2) root.getChildren().remove(2); - root.getChildren().add(2, disputeChat); + root.getChildren().add(2, chatView); } addListenersOnSelectDispute(); } - private void onCloseDispute(Dispute dispute) { + protected abstract void handleOnSelectDispute(Dispute dispute); + + protected void onCloseDispute(Dispute dispute) { long protocolVersion = dispute.getContract().getOfferPayload().getProtocolVersion(); if (protocolVersion == Version.TRADE_PROTOCOL_VERSION) { - disputeSummaryWindow.onFinalizeDispute(() -> disputeChat.removeInputBox()) + disputeSummaryWindow.onFinalizeDispute(() -> chatView.removeInputBox()) .show(dispute); } else { new Popup<>() diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/arbitrator/ArbitratorDisputeView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java similarity index 63% rename from desktop/src/main/java/bisq/desktop/main/disputes/arbitrator/ArbitratorDisputeView.java rename to desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java index f21f53fa39..ae6a41a89c 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/arbitrator/ArbitratorDisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java @@ -15,44 +15,45 @@ * along with Bisq. If not, see . */ -package bisq.desktop.main.disputes.arbitrator; +package bisq.desktop.main.support.dispute.agent; import bisq.desktop.common.view.FxmlView; -import bisq.desktop.main.disputes.trader.TraderDisputeView; +import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; +import bisq.desktop.main.support.dispute.DisputeView; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; import bisq.core.app.AppOptionKeys; -import bisq.core.arbitration.DisputeManager; +import bisq.core.locale.Res; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeSession; import bisq.core.trade.TradeManager; import bisq.core.util.BSFormatter; -import bisq.network.p2p.P2PService; - import bisq.common.crypto.KeyRing; import com.google.inject.name.Named; -import javax.inject.Inject; +import javafx.scene.control.Button; @FxmlView -public class ArbitratorDisputeView extends TraderDisputeView { +public abstract class DisputeAgentView extends DisputeView { - @Inject - public ArbitratorDisputeView(DisputeManager disputeManager, - KeyRing keyRing, - TradeManager tradeManager, - BSFormatter formatter, - DisputeSummaryWindow disputeSummaryWindow, - PrivateNotificationManager privateNotificationManager, - ContractWindow contractWindow, - TradeDetailsWindow tradeDetailsWindow, - P2PService p2PService, - AccountAgeWitnessService accountAgeWitnessService, - @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + public DisputeAgentView(DisputeManager> disputeManager, + KeyRing keyRing, + TradeManager tradeManager, + BSFormatter formatter, + DisputeSummaryWindow disputeSummaryWindow, + PrivateNotificationManager privateNotificationManager, + ContractWindow contractWindow, + TradeDetailsWindow tradeDetailsWindow, + AccountAgeWitnessService accountAgeWitnessService, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(disputeManager, keyRing, tradeManager, @@ -61,7 +62,6 @@ public class ArbitratorDisputeView extends TraderDisputeView { privateNotificationManager, contractWindow, tradeDetailsWindow, - p2PService, accountAgeWitnessService, useDevPrivilegeKeys); } @@ -89,11 +89,21 @@ public class ArbitratorDisputeView extends TraderDisputeView { matchesBuyersPaymentAccountData || matchesSellersPaymentAccountData; boolean open = !dispute.isClosed() && filterString.toLowerCase().equals("open"); - boolean isMyCase = dispute.getArbitratorPubKeyRing().equals(keyRing.getPubKeyRing()); + boolean isMyCase = dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing()); return isMyCase && (open || filterString.isEmpty() || anyMatch); }); } + @Override + protected void handleOnSelectDispute(Dispute dispute) { + Button closeDisputeButton = null; + if (!dispute.isClosed() && !disputeManager.isTrader(dispute)) { + closeDisputeButton = new AutoTooltipButton(Res.get("support.closeTicket")); + closeDisputeButton.setOnAction(e -> onCloseDispute(getSelectedDispute())); + } + DisputeSession chatSession = getConcreteDisputeChatSession(dispute); + chatView.display(chatSession, closeDisputeButton, root.widthProperty()); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.fxml b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.fxml similarity index 90% rename from desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.fxml rename to desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.fxml index 8afdc69695..82b341ce25 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/trader/TraderDisputeView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.fxml @@ -19,7 +19,7 @@ - @@ -27,3 +27,4 @@ + diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java new file mode 100644 index 0000000000..c84baa6270 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/arbitration/ArbitratorView.java @@ -0,0 +1,78 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.main.support.dispute.agent.arbitration; + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.windows.ContractWindow; +import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; +import bisq.desktop.main.overlays.windows.TradeDetailsWindow; +import bisq.desktop.main.support.dispute.agent.DisputeAgentView; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.app.AppOptionKeys; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeSession; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.arbitration.ArbitrationSession; +import bisq.core.trade.TradeManager; +import bisq.core.util.BSFormatter; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.name.Named; + +import javax.inject.Inject; + +@FxmlView +public class ArbitratorView extends DisputeAgentView { + + @Inject + public ArbitratorView(ArbitrationManager arbitrationManager, + KeyRing keyRing, + TradeManager tradeManager, + BSFormatter formatter, + DisputeSummaryWindow disputeSummaryWindow, + PrivateNotificationManager privateNotificationManager, + ContractWindow contractWindow, + TradeDetailsWindow tradeDetailsWindow, + AccountAgeWitnessService accountAgeWitnessService, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(arbitrationManager, + keyRing, + tradeManager, + formatter, + disputeSummaryWindow, + privateNotificationManager, + contractWindow, + tradeDetailsWindow, + accountAgeWitnessService, + useDevPrivilegeKeys); + } + + @Override + protected SupportType getType() { + return SupportType.ARBITRATION; + } + + @Override + protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { + return new ArbitrationSession(dispute, disputeManager.isTrader(dispute)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/disputes/arbitrator/ArbitratorDisputeView.fxml b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/mediation/MediatorView.fxml similarity index 90% rename from desktop/src/main/java/bisq/desktop/main/disputes/arbitrator/ArbitratorDisputeView.fxml rename to desktop/src/main/java/bisq/desktop/main/support/dispute/agent/mediation/MediatorView.fxml index 7876f9c7bd..add07073d7 100644 --- a/desktop/src/main/java/bisq/desktop/main/disputes/arbitrator/ArbitratorDisputeView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/mediation/MediatorView.fxml @@ -19,7 +19,7 @@ - @@ -27,4 +27,3 @@ - diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/mediation/MediatorView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/mediation/MediatorView.java new file mode 100644 index 0000000000..16d87143ce --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/mediation/MediatorView.java @@ -0,0 +1,78 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.main.support.dispute.agent.mediation; + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.windows.ContractWindow; +import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; +import bisq.desktop.main.overlays.windows.TradeDetailsWindow; +import bisq.desktop.main.support.dispute.agent.DisputeAgentView; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.app.AppOptionKeys; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeSession; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.mediation.MediationSession; +import bisq.core.trade.TradeManager; +import bisq.core.util.BSFormatter; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.name.Named; + +import javax.inject.Inject; + +@FxmlView +public class MediatorView extends DisputeAgentView { + + @Inject + public MediatorView(MediationManager mediationManager, + KeyRing keyRing, + TradeManager tradeManager, + BSFormatter formatter, + DisputeSummaryWindow disputeSummaryWindow, + PrivateNotificationManager privateNotificationManager, + ContractWindow contractWindow, + TradeDetailsWindow tradeDetailsWindow, + AccountAgeWitnessService accountAgeWitnessService, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(mediationManager, + keyRing, + tradeManager, + formatter, + disputeSummaryWindow, + privateNotificationManager, + contractWindow, + tradeDetailsWindow, + accountAgeWitnessService, + useDevPrivilegeKeys); + } + + @Override + protected SupportType getType() { + return SupportType.MEDIATION; + } + + @Override + protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { + return new MediationSession(dispute, disputeManager.isTrader(dispute)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/DisputeClientView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/DisputeClientView.java new file mode 100644 index 0000000000..4575b2e580 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/DisputeClientView.java @@ -0,0 +1,58 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.main.support.dispute.client; + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.windows.ContractWindow; +import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; +import bisq.desktop.main.overlays.windows.TradeDetailsWindow; +import bisq.desktop.main.support.dispute.DisputeView; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeSession; +import bisq.core.trade.TradeManager; +import bisq.core.util.BSFormatter; + +import bisq.common.crypto.KeyRing; + +@FxmlView +public abstract class DisputeClientView extends DisputeView { + public DisputeClientView(DisputeManager> DisputeManager, + KeyRing keyRing, + TradeManager tradeManager, + BSFormatter formatter, + DisputeSummaryWindow disputeSummaryWindow, + PrivateNotificationManager privateNotificationManager, + ContractWindow contractWindow, + TradeDetailsWindow tradeDetailsWindow, + AccountAgeWitnessService accountAgeWitnessService, + boolean useDevPrivilegeKeys) { + super(DisputeManager, keyRing, tradeManager, formatter, disputeSummaryWindow, privateNotificationManager, + contractWindow, tradeDetailsWindow, accountAgeWitnessService, useDevPrivilegeKeys); + } + + @Override + protected void handleOnSelectDispute(Dispute dispute) { + DisputeSession chatSession = getConcreteDisputeChatSession(dispute); + chatView.display(chatSession, root.widthProperty()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.fxml b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.fxml new file mode 100644 index 0000000000..5d9fb0c65d --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.fxml @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.java new file mode 100644 index 0000000000..0b81ab83af --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/arbitration/ArbitrationClientView.java @@ -0,0 +1,70 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.main.support.dispute.client.arbitration; + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.windows.ContractWindow; +import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; +import bisq.desktop.main.overlays.windows.TradeDetailsWindow; +import bisq.desktop.main.support.dispute.client.DisputeClientView; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.app.AppOptionKeys; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeSession; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.arbitration.ArbitrationSession; +import bisq.core.trade.TradeManager; +import bisq.core.util.BSFormatter; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.name.Named; + +import javax.inject.Inject; + +@FxmlView +public class ArbitrationClientView extends DisputeClientView { + @Inject + public ArbitrationClientView(ArbitrationManager arbitrationManager, + KeyRing keyRing, + TradeManager tradeManager, + BSFormatter formatter, + DisputeSummaryWindow disputeSummaryWindow, + PrivateNotificationManager privateNotificationManager, + ContractWindow contractWindow, + TradeDetailsWindow tradeDetailsWindow, + AccountAgeWitnessService accountAgeWitnessService, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(arbitrationManager, keyRing, tradeManager, formatter, disputeSummaryWindow, + privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, + useDevPrivilegeKeys); + } + + @Override + protected SupportType getType() { + return SupportType.ARBITRATION; + } + + @Override + protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { + return new ArbitrationSession(dispute, disputeManager.isTrader(dispute)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/mediation/MediationClientView.fxml b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/mediation/MediationClientView.fxml new file mode 100644 index 0000000000..b0feee5558 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/mediation/MediationClientView.fxml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/mediation/MediationClientView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/mediation/MediationClientView.java new file mode 100644 index 0000000000..551fdef668 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/mediation/MediationClientView.java @@ -0,0 +1,70 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.main.support.dispute.client.mediation; + +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.main.overlays.windows.ContractWindow; +import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; +import bisq.desktop.main.overlays.windows.TradeDetailsWindow; +import bisq.desktop.main.support.dispute.client.DisputeClientView; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.alert.PrivateNotificationManager; +import bisq.core.app.AppOptionKeys; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeSession; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.mediation.MediationSession; +import bisq.core.trade.TradeManager; +import bisq.core.util.BSFormatter; + +import bisq.common.crypto.KeyRing; + +import com.google.inject.name.Named; + +import javax.inject.Inject; + +@FxmlView +public class MediationClientView extends DisputeClientView { + @Inject + public MediationClientView(MediationManager mediationManager, + KeyRing keyRing, + TradeManager tradeManager, + BSFormatter formatter, + DisputeSummaryWindow disputeSummaryWindow, + PrivateNotificationManager privateNotificationManager, + ContractWindow contractWindow, + TradeDetailsWindow tradeDetailsWindow, + AccountAgeWitnessService accountAgeWitnessService, + @Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { + super(mediationManager, keyRing, tradeManager, formatter, disputeSummaryWindow, + privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, + useDevPrivilegeKeys); + } + + @Override + protected SupportType getType() { + return SupportType.MEDIATION; + } + + @Override + protected DisputeSession getConcreteDisputeChatSession(Dispute dispute) { + return new MediationSession(dispute, disputeManager.isTrader(dispute)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index 1afbc53433..abb0b04975 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -17,10 +17,14 @@ package bisq.desktop.util; +import bisq.desktop.Navigation; import bisq.desktop.app.BisqApp; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.BisqTextArea; import bisq.desktop.components.indicator.TxConfidenceIndicator; +import bisq.desktop.main.MainView; +import bisq.desktop.main.account.AccountView; +import bisq.desktop.main.account.content.fiataccounts.FiatAccountsView; import bisq.desktop.main.overlays.popups.Popup; import bisq.core.app.BisqEnvironment; @@ -195,8 +199,12 @@ public class GUIUtil { } } - public static void exportAccounts(ArrayList accounts, String fileName, - Preferences preferences, Stage stage, PersistenceProtoResolver persistenceProtoResolver, CorruptedDatabaseFilesHandler corruptedDatabaseFilesHandler) { + public static void exportAccounts(ArrayList accounts, + String fileName, + Preferences preferences, + Stage stage, + PersistenceProtoResolver persistenceProtoResolver, + CorruptedDatabaseFilesHandler corruptedDatabaseFilesHandler) { if (!accounts.isEmpty()) { String directory = getDirectoryFromChooser(preferences, stage); if (directory != null && !directory.isEmpty()) { @@ -210,8 +218,12 @@ public class GUIUtil { } } - public static void importAccounts(User user, String fileName, Preferences preferences, Stage stage, - PersistenceProtoResolver persistenceProtoResolver, CorruptedDatabaseFilesHandler corruptedDatabaseFilesHandler) { + public static void importAccounts(User user, + String fileName, + Preferences preferences, + Stage stage, + PersistenceProtoResolver persistenceProtoResolver, + CorruptedDatabaseFilesHandler corruptedDatabaseFilesHandler) { FileChooser fileChooser = new FileChooser(); File initDir = new File(preferences.getDirectoryChooserPath()); if (initDir.isDirectory()) { @@ -357,7 +369,8 @@ public class GUIUtil { }; } - public static Callback, ListCell> getCurrencyListItemCellFactory(String postFixSingle, String postFixMulti, + public static Callback, ListCell> getCurrencyListItemCellFactory(String postFixSingle, + String postFixMulti, Preferences preferences) { return p -> new ListCell<>() { @Override @@ -589,7 +602,9 @@ public class GUIUtil { }; } - public static void updateConfidence(TransactionConfidence confidence, Tooltip tooltip, TxConfidenceIndicator txConfidenceIndicator) { + public static void updateConfidence(TransactionConfidence confidence, + Tooltip tooltip, + TxConfidenceIndicator txConfidenceIndicator) { if (confidence != null) { switch (confidence.getConfidenceType()) { case UNKNOWN: @@ -741,21 +756,56 @@ public class GUIUtil { ""; } - public static boolean isReadyForTxBroadcast(P2PService p2PService, WalletsSetup walletsSetup) { - return p2PService.isBootstrapped() && - walletsSetup.isDownloadComplete() && - walletsSetup.hasSufficientPeersForBroadcast(); + public static boolean isBootstrappedOrShowPopup(P2PService p2PService) { + if (!p2PService.isBootstrapped()) { + new Popup<>().information(Res.get("popup.warning.notFullyConnected")).show(); + return false; + } + + return true; } - public static void showNotReadyForTxBroadcastPopups(P2PService p2PService, WalletsSetup walletsSetup) { - if (!p2PService.isBootstrapped()) - new Popup<>().information(Res.get("popup.warning.notFullyConnected")).show(); - else if (!walletsSetup.hasSufficientPeersForBroadcast()) + public static boolean isReadyForTxBroadcastOrShowPopup(P2PService p2PService, WalletsSetup walletsSetup) { + if (!GUIUtil.isBootstrappedOrShowPopup(p2PService)) { + return false; + } + + if (!walletsSetup.hasSufficientPeersForBroadcast()) { new Popup<>().information(Res.get("popup.warning.notSufficientConnectionsToBtcNetwork", walletsSetup.getMinBroadcastConnections())).show(); - else if (!walletsSetup.isDownloadComplete()) + return false; + } + + if (!walletsSetup.isDownloadComplete()) { new Popup<>().information(Res.get("popup.warning.downloadNotComplete")).show(); - else - log.warn("showNotReadyForTxBroadcastPopups called but no case matched. This should never happen if isReadyForTxBroadcast was called before."); + return false; + } + + return true; + } + + public static boolean canCreateOrTakeOfferOrShowPopup(User user, Navigation navigation) { + if (!user.hasAcceptedArbitrators()) { + new Popup<>().warning(Res.get("popup.warning.noArbitratorsAvailable")).show(); + return false; + } + + if (!user.hasAcceptedMediators()) { + new Popup<>().warning(Res.get("popup.warning.noMediatorsAvailable")).show(); + return false; + } + + if (user.currentPaymentAccountProperty().get() == null) { + new Popup<>().headLine(Res.get("popup.warning.noTradingAccountSetup.headline")) + .instruction(Res.get("popup.warning.noTradingAccountSetup.msg")) + .actionButtonTextWithGoTo("navigation.account") + .onAction(() -> { + navigation.setReturnPath(navigation.getCurrentPath()); + navigation.navigateTo(MainView.class, AccountView.class, FiatAccountsView.class); + }).show(); + return false; + } + + return true; } public static void showWantToBurnBTCPopup(Coin miningFee, Coin amount, BSFormatter btcFormatter) { @@ -861,8 +911,13 @@ public class GUIUtil { } } - public static void showBsqFeeInfoPopup(Coin fee, Coin miningFee, Coin btcForIssuance, int txSize, BsqFormatter bsqFormatter, - BSFormatter btcFormatter, String type, + public static void showBsqFeeInfoPopup(Coin fee, + Coin miningFee, + Coin btcForIssuance, + int txSize, + BsqFormatter bsqFormatter, + BSFormatter btcFormatter, + String type, Runnable actionHandler) { String confirmationMessage; @@ -899,7 +954,11 @@ public class GUIUtil { showBsqFeeInfoPopup(fee, miningFee, null, txSize, bsqFormatter, btcFormatter, type, actionHandler); } - public static void setFitToRowsForTableView(TableView tableView, int rowHeight, int headerHeight, int minNumRows, int maxNumRows) { + public static void setFitToRowsForTableView(TableView tableView, + int rowHeight, + int headerHeight, + int minNumRows, + int maxNumRows) { int size = tableView.getItems().size(); int minHeight = rowHeight * minNumRows + headerHeight; int maxHeight = rowHeight * maxNumRows + headerHeight; @@ -1005,7 +1064,9 @@ public class GUIUtil { } @NotNull - public static ListCell getComboBoxButtonCell(String title, ComboBox comboBox, Boolean hideOriginalPrompt) { + public static ListCell getComboBoxButtonCell(String title, + ComboBox comboBox, + Boolean hideOriginalPrompt) { return new ListCell<>() { @Override protected void updateItem(T item, boolean empty) { diff --git a/desktop/src/test/java/bisq/desktop/GuiceSetupTest.java b/desktop/src/test/java/bisq/desktop/GuiceSetupTest.java index 482ab24233..7b0f54a5ee 100644 --- a/desktop/src/test/java/bisq/desktop/GuiceSetupTest.java +++ b/desktop/src/test/java/bisq/desktop/GuiceSetupTest.java @@ -36,6 +36,15 @@ import bisq.core.payment.ChargeBackRisk; import bisq.core.payment.TradeLimits; import bisq.core.proto.network.CoreNetworkProtoResolver; import bisq.core.proto.persistable.CorePersistenceProtoResolver; +import bisq.core.support.dispute.arbitration.ArbitrationDisputeListService; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorService; +import bisq.core.support.dispute.mediation.MediationDisputeListService; +import bisq.core.support.dispute.mediation.MediationManager; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.mediation.mediator.MediatorService; +import bisq.core.support.traderchat.TraderChatManager; import bisq.core.user.Preferences; import bisq.core.user.User; import bisq.core.util.BSFormatter; @@ -133,6 +142,15 @@ public class GuiceSetupTest { assertSingleton(PriceAlert.class); assertSingleton(MarketAlerts.class); assertSingleton(ChargeBackRisk.class); + assertSingleton(ArbitratorService.class); + assertSingleton(ArbitratorManager.class); + assertSingleton(ArbitrationManager.class); + assertSingleton(ArbitrationDisputeListService.class); + assertSingleton(MediatorService.class); + assertSingleton(MediatorManager.class); + assertSingleton(MediationManager.class); + assertSingleton(MediationDisputeListService.class); + assertSingleton(TraderChatManager.class); assertNotSingleton(Storage.class); } diff --git a/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactoryTest.java b/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactoryTest.java index a939f3c802..8b93668bc3 100644 --- a/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactoryTest.java +++ b/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradableFactoryTest.java @@ -17,7 +17,7 @@ package bisq.desktop.main.funds.transactions; -import bisq.core.arbitration.DisputeManager; +import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.offer.OpenOffer; import bisq.core.trade.Tradable; import bisq.core.trade.Trade; @@ -32,9 +32,9 @@ import static org.mockito.Mockito.mock; public class TransactionAwareTradableFactoryTest { @Test public void testCreateWhenNotOpenOfferOrTrade() { - DisputeManager manager = mock(DisputeManager.class); + ArbitrationManager arbitrationManager = mock(ArbitrationManager.class); - TransactionAwareTradableFactory factory = new TransactionAwareTradableFactory(manager); + TransactionAwareTradableFactory factory = new TransactionAwareTradableFactory(arbitrationManager); Tradable delegate = mock(Tradable.class); assertFalse(delegate instanceof OpenOffer); diff --git a/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradeTest.java b/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradeTest.java index b2ae9e5d37..66d26038ec 100644 --- a/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradeTest.java +++ b/desktop/src/test/java/bisq/desktop/main/funds/transactions/TransactionAwareTradeTest.java @@ -17,8 +17,8 @@ package bisq.desktop.main.funds.transactions; -import bisq.core.arbitration.Dispute; -import bisq.core.arbitration.DisputeManager; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.trade.Trade; import org.bitcoinj.core.Transaction; @@ -39,7 +39,7 @@ public class TransactionAwareTradeTest { private static final String XID = "123"; private Transaction transaction; - private DisputeManager manager; + private ArbitrationManager arbitrationManager; private Trade delegate; private TransactionAwareTradable trade; @@ -49,8 +49,8 @@ public class TransactionAwareTradeTest { when(transaction.getHashAsString()).thenReturn(XID); this.delegate = mock(Trade.class, RETURNS_DEEP_STUBS); - this.manager = mock(DisputeManager.class, RETURNS_DEEP_STUBS); - this.trade = new TransactionAwareTrade(this.delegate, this.manager); + this.arbitrationManager = mock(ArbitrationManager.class, RETURNS_DEEP_STUBS); + this.trade = new TransactionAwareTrade(this.delegate, this.arbitrationManager); } @Test @@ -85,7 +85,7 @@ public class TransactionAwareTradeTest { when(dispute.getDisputePayoutTxId()).thenReturn(XID); when(dispute.getTradeId()).thenReturn(tradeId); - when(manager.getDisputesAsObservableList()) + when(arbitrationManager.getDisputesAsObservableList()) .thenReturn(FXCollections.observableArrayList(Collections.singleton(dispute))); when(delegate.getId()).thenReturn(tradeId); diff --git a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModelTest.java index e2291be163..e588735a91 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModelTest.java @@ -61,7 +61,7 @@ public class CreateOfferDataModelTest { null, preferences, user, null, null, priceFeedService, null, null, feeService, feeEstimationService, - null, null, makerFeeProvider); + null, null, makerFeeProvider, null); } @Test diff --git a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java index 6b4aa52116..6fcb5950ba 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java @@ -104,11 +104,16 @@ public class CreateOfferViewModelTest { when(bsqFormatter.formatCoin(any())).thenReturn("0"); when(bsqWalletService.getAvailableConfirmedBalance()).thenReturn(Coin.ZERO); - CreateOfferDataModel dataModel = new CreateOfferDataModel(null, btcWalletService, bsqWalletService, empty, user, null, null, priceFeedService, null, accountAgeWitnessService, feeService, txFeeEstimationService, null, bsFormatter, mock(MakerFeeProvider.class)); + CreateOfferDataModel dataModel = new CreateOfferDataModel(null, btcWalletService, + bsqWalletService, empty, user, null, null, priceFeedService, null, + accountAgeWitnessService, feeService, txFeeEstimationService, + null, bsFormatter, mock(MakerFeeProvider.class), null); dataModel.initWithData(OfferPayload.Direction.BUY, new CryptoCurrency("BTC", "bitcoin")); dataModel.activate(); - model = new CreateOfferViewModel(dataModel, null, fiatPriceValidator, altcoinValidator, btcValidator, null, securityDepositValidator, null, null, priceFeedService, null, preferences, bsFormatter, bsqFormatter); + model = new CreateOfferViewModel(dataModel, null, fiatPriceValidator, altcoinValidator, + btcValidator, null, securityDepositValidator, priceFeedService, null, + preferences, bsFormatter, bsqFormatter); model.activate(); } diff --git a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java index 272d241c46..d0224d2977 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/offerbook/OfferBookViewModelTest.java @@ -446,7 +446,10 @@ public class OfferBookViewModelTest { return paymentAccount; } - private PaymentAccount getSepaAccount(String currencyCode, String countryCode, String bic, ArrayList countryCodes) { + private PaymentAccount getSepaAccount(String currencyCode, + String countryCode, + String bic, + ArrayList countryCodes) { CountryBasedPaymentAccount paymentAccount = new SepaAccount(); paymentAccount.setSingleTradeCurrency(new FiatCurrency(currencyCode)); paymentAccount.setCountry(new Country(countryCode, null, null)); @@ -471,7 +474,10 @@ public class OfferBookViewModelTest { return paymentAccount; } - private PaymentAccount getSpecificBanksAccount(String currencyCode, String countryCode, String bankId, ArrayList bankIds) { + private PaymentAccount getSpecificBanksAccount(String currencyCode, + String countryCode, + String bankId, + ArrayList bankIds) { SpecificBanksAccount paymentAccount = new SpecificBanksAccount(); paymentAccount.setSingleTradeCurrency(new FiatCurrency(currencyCode)); paymentAccount.setCountry(new Country(countryCode, null, null)); @@ -499,7 +505,10 @@ public class OfferBookViewModelTest { null); } - private Offer getSEPAPaymentMethod(String currencyCode, String countryCode, ArrayList countryCodes, String bankId) { + private Offer getSEPAPaymentMethod(String currencyCode, + String countryCode, + ArrayList countryCodes, + String bankId) { return getPaymentMethod(currencyCode, PaymentMethod.SEPA_ID, countryCode, @@ -526,7 +535,10 @@ public class OfferBookViewModelTest { new ArrayList<>(Collections.singletonList(bankId))); } - private Offer getSpecificBanksPaymentMethod(String currencyCode, String countryCode, String bankId, ArrayList bankIds) { + private Offer getSpecificBanksPaymentMethod(String currencyCode, + String countryCode, + String bankId, + ArrayList bankIds) { return getPaymentMethod(currencyCode, PaymentMethod.SPECIFIC_BANKS_ID, countryCode, @@ -535,7 +547,12 @@ public class OfferBookViewModelTest { bankIds); } - private Offer getPaymentMethod(String currencyCode, String paymentMethodId, String countryCode, ArrayList countryCodes, String bankId, ArrayList bankIds) { + private Offer getPaymentMethod(String currencyCode, + String paymentMethodId, + String countryCode, + ArrayList countryCodes, + String bankId, + ArrayList bankIds) { return getOffer(currencyCode, paymentMethodId, countryCode, @@ -545,7 +562,12 @@ public class OfferBookViewModelTest { } - private Offer getOffer(String tradeCurrencyCode, String paymentMethodId, String countryCode, ArrayList acceptedCountryCodes, String bankId, ArrayList acceptedBanks) { + private Offer getOffer(String tradeCurrencyCode, + String paymentMethodId, + String countryCode, + ArrayList acceptedCountryCodes, + String bankId, + ArrayList acceptedBanks) { return new Offer(new OfferPayload(null, 0, null, diff --git a/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java b/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java index ee0dc5a2e9..776aa627a1 100644 --- a/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModelTest.java @@ -93,7 +93,7 @@ public class EditOfferDataModelTest { btcWalletService, bsqWalletService, empty, user, null, null, priceFeedService, null, accountAgeWitnessService, feeService, null, null, - null, null, mock(MakerFeeProvider.class)); + null, null, mock(MakerFeeProvider.class), null); } @Test diff --git a/desktop/src/test/java/bisq/desktop/main/settings/preferences/PreferencesViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/settings/preferences/PreferencesViewModelTest.java index 3f3fc0b2cd..441c12a357 100644 --- a/desktop/src/test/java/bisq/desktop/main/settings/preferences/PreferencesViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/settings/preferences/PreferencesViewModelTest.java @@ -19,8 +19,8 @@ package bisq.desktop.main.settings.preferences; import bisq.desktop.maker.PreferenceMakers; -import bisq.core.arbitration.Arbitrator; -import bisq.core.arbitration.ArbitratorManager; +import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; +import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.user.Preferences; import bisq.network.p2p.NodeAddress; @@ -70,7 +70,7 @@ public class PreferencesViewModelTest { Preferences preferences = PreferenceMakers.empty; - when(arbitratorManager.getArbitratorsObservableMap()).thenReturn(arbitrators); + when(arbitratorManager.getObservableMap()).thenReturn(arbitrators); PreferencesViewModel model = new PreferencesViewModel(preferences, arbitratorManager); diff --git a/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java b/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java index d379fb2fde..2b7fb9097b 100644 --- a/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java +++ b/monitor/src/main/java/bisq/monitor/metric/P2PSeedNodeSnapshotBase.java @@ -58,7 +58,7 @@ import org.jetbrains.annotations.NotNull; * */ @Slf4j -abstract public class P2PSeedNodeSnapshotBase extends Metric implements MessageListener { +public abstract class P2PSeedNodeSnapshotBase extends Metric implements MessageListener { private static final String HOSTS = "run.hosts"; private static final String TOR_PROXY_PORT = "run.torProxyPort"; @@ -103,7 +103,7 @@ abstract public class P2PSeedNodeSnapshotBase extends Metric implements MessageL report(); } - abstract protected List getRequests(); + protected abstract List getRequests(); protected void send(NetworkNode networkNode, NetworkEnvelope message) { @@ -172,5 +172,5 @@ abstract public class P2PSeedNodeSnapshotBase extends Metric implements MessageL connection.removeMessageListener(this); } - abstract protected boolean treatMessage(NetworkEnvelope networkEnvelope, Connection connection); + protected abstract boolean treatMessage(NetworkEnvelope networkEnvelope, Connection connection); } diff --git a/p2p/src/main/java/bisq/network/p2p/AckMessageSourceType.java b/p2p/src/main/java/bisq/network/p2p/AckMessageSourceType.java index adfddf681e..679c138459 100644 --- a/p2p/src/main/java/bisq/network/p2p/AckMessageSourceType.java +++ b/p2p/src/main/java/bisq/network/p2p/AckMessageSourceType.java @@ -21,5 +21,7 @@ public enum AckMessageSourceType { UNDEFINED, OFFER_MESSAGE, TRADE_MESSAGE, - DISPUTE_MESSAGE + ARBITRATION_MESSAGE, + MEDIATION_MESSAGE, + TRADE_CHAT_MESSAGE } diff --git a/p2p/src/main/java/bisq/network/p2p/BootstrapListener.java b/p2p/src/main/java/bisq/network/p2p/BootstrapListener.java index f3c161f5ba..3b82d4945b 100644 --- a/p2p/src/main/java/bisq/network/p2p/BootstrapListener.java +++ b/p2p/src/main/java/bisq/network/p2p/BootstrapListener.java @@ -44,7 +44,7 @@ public abstract class BootstrapListener implements P2PServiceListener { } @Override - abstract public void onUpdatedDataReceived(); + public abstract void onUpdatedDataReceived(); @Override public void onRequestCustomBridges() { diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java index 94262d8f7f..82b30c46ef 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java @@ -94,7 +94,7 @@ public abstract class NetworkNode implements MessageListener { // Calls this (and other registered) setup listener's ``onTorNodeReady()`` and ``onHiddenServicePublished`` // when the events happen. - abstract public void start(@Nullable SetupListener setupListener); + public abstract void start(@Nullable SetupListener setupListener); public SettableFuture sendMessage(@NotNull NodeAddress peersNodeAddress, NetworkEnvelope networkEnvelope) { @@ -462,7 +462,7 @@ public abstract class NetworkNode implements MessageListener { log.debug(sb.toString()); } - abstract protected Socket createSocket(NodeAddress peersNodeAddress) throws IOException; + protected abstract Socket createSocket(NodeAddress peersNodeAddress) throws IOException; @Nullable public NodeAddress getNodeAddress() { diff --git a/p2p/src/main/java/bisq/network/p2p/storage/persistence/MapStoreService.java b/p2p/src/main/java/bisq/network/p2p/storage/persistence/MapStoreService.java index 7b87201bd4..5449a3f3a9 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/persistence/MapStoreService.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/persistence/MapStoreService.java @@ -52,9 +52,9 @@ public abstract class MapStoreService getMap(); + public abstract Map getMap(); - abstract public boolean canHandle(R payload); + public abstract boolean canHandle(R payload); R putIfAbsent(P2PDataStorage.ByteArray hash, R payload) { R previous = getMap().putIfAbsent(hash, payload); diff --git a/p2p/src/main/java/bisq/network/p2p/storage/persistence/StoreService.java b/p2p/src/main/java/bisq/network/p2p/storage/persistence/StoreService.java index 81875d9c71..5476ad9403 100644 --- a/p2p/src/main/java/bisq/network/p2p/storage/persistence/StoreService.java +++ b/p2p/src/main/java/bisq/network/p2p/storage/persistence/StoreService.java @@ -73,7 +73,7 @@ public abstract class StoreService { return store; } - abstract public String getFileName(); + public abstract String getFileName(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -133,5 +133,5 @@ public abstract class StoreService { } } - abstract protected T createStore(); + protected abstract T createStore(); }