diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index 1485ae2546..ce52e74e67 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -73,6 +73,7 @@ import bisq.core.dao.state.model.governance.Vote; import bisq.asset.Asset; +import bisq.common.config.Config; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ExceptionHandler; import bisq.common.handlers.ResultHandler; @@ -95,9 +96,14 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import lombok.extern.slf4j.Slf4j; @@ -423,10 +429,18 @@ public class DaoFacade implements DaoSetupService { case RESULT: break; } - return firstBlock; } + public Map getBlockStartDateByCycleIndex() { + AtomicInteger index = new AtomicInteger(); + Map map = new HashMap<>(); + periodService.getCycles() + .forEach(cycle -> daoStateService.getBlockAtHeight(cycle.getHeightOfFirstBlock()) + .ifPresent(block -> map.put(index.getAndIncrement(), new Date(block.getTime())))); + return map; + } + // Because last block in request and voting phases must not be used for making a tx as it will get confirmed in the // next block which would be already the next phase we hide that last block to the user and add it to the break. public int getLastBlockOfPhaseForDisplay(int height, DaoPhase.Phase phase) { @@ -750,4 +764,32 @@ public class DaoFacade implements DaoSetupService { long baseFactor = daoStateService.getParamValueAsCoin(Param.BONDED_ROLE_FACTOR, height).value; return requiredBondUnit * baseFactor; } + + public Set getAllPastParamValues(Param param) { + Set set = new HashSet<>(); + periodService.getCycles().forEach(cycle -> { + set.add(getParamValue(param, cycle.getHeightOfFirstBlock())); + }); + return set; + } + + public Set getAllDonationAddresses() { + // We support any of the past addresses as well as in case the peer has not enabled the DAO or is out of sync we + // do not want to break validation. + Set allPastParamValues = getAllPastParamValues(Param.RECIPIENT_BTC_ADDRESS); + + // If Dao is deactivated we need to add the default address as getAllPastParamValues will not return us any. + if (allPastParamValues.isEmpty()) { + allPastParamValues.add(Param.RECIPIENT_BTC_ADDRESS.getDefaultValue()); + } + + if (Config.baseCurrencyNetwork().isMainnet()) { + // If Dao is deactivated we need to add the past addresses used as well. + // This list need to be updated once a new address gets defined. + allPastParamValues.add("3EtUWqsGThPtjwUczw27YCo6EWvQdaPUyp"); // burning man 2019 + allPastParamValues.add("3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV"); // burningman2 + } + + return allPastParamValues; + } } diff --git a/core/src/main/java/bisq/core/support/dispute/Dispute.java b/core/src/main/java/bisq/core/support/dispute/Dispute.java index d5f45a7a18..2cfd0850d7 100644 --- a/core/src/main/java/bisq/core/support/dispute/Dispute.java +++ b/core/src/main/java/bisq/core/support/dispute/Dispute.java @@ -44,6 +44,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; import lombok.EqualsAndHashCode; @@ -103,6 +104,15 @@ public final class Dispute implements NetworkPayload { @Nullable private String delayedPayoutTxId; + // Added at v1.3.9 + @Setter + @Nullable + private String donationAddressOfDelayedPayoutTx; + // We do not persist uid, it is only used by dispute agents to guarantee an uid. + @Setter + @Nullable + private transient String uid; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -192,6 +202,7 @@ public final class Dispute implements NetworkPayload { this.supportType = supportType; id = tradeId + "_" + traderId; + uid = UUID.randomUUID().toString(); } @Override @@ -228,6 +239,7 @@ public final class Dispute implements NetworkPayload { Optional.ofNullable(supportType).ifPresent(result -> builder.setSupportType(SupportType.toProtoMessage(supportType))); Optional.ofNullable(mediatorsDisputeResult).ifPresent(result -> builder.setMediatorsDisputeResult(mediatorsDisputeResult)); Optional.ofNullable(delayedPayoutTxId).ifPresent(result -> builder.setDelayedPayoutTxId(delayedPayoutTxId)); + Optional.ofNullable(donationAddressOfDelayedPayoutTx).ifPresent(result -> builder.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx)); return builder.build(); } @@ -271,6 +283,11 @@ public final class Dispute implements NetworkPayload { dispute.setDelayedPayoutTxId(delayedPayoutTxId); } + String donationAddressOfDelayedPayoutTx = proto.getDonationAddressOfDelayedPayoutTx(); + if (!donationAddressOfDelayedPayoutTx.isEmpty()) { + dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx); + } + return dispute; } @@ -357,6 +374,7 @@ public final class Dispute implements NetworkPayload { return "Dispute{" + "\n tradeId='" + tradeId + '\'' + ",\n id='" + id + '\'' + + ",\n uid='" + uid + '\'' + ",\n traderId=" + traderId + ",\n disputeOpenerIsBuyer=" + disputeOpenerIsBuyer + ",\n disputeOpenerIsMaker=" + disputeOpenerIsMaker + @@ -382,6 +400,7 @@ public final class Dispute implements NetworkPayload { ",\n supportType=" + supportType + ",\n mediatorsDisputeResult='" + mediatorsDisputeResult + '\'' + ",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' + + ",\n donationAddressOfDelayedPayoutTx='" + donationAddressOfDelayedPayoutTx + '\'' + "\n}"; } } diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 9998e1f2eb..bd1a33d0cc 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -21,6 +21,7 @@ import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.Restrictions; import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.DaoFacade; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.monetary.Altcoin; @@ -36,6 +37,7 @@ 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.TradeDataValidation; import bisq.core.trade.TradeManager; import bisq.core.trade.closed.ClosedTradableManager; @@ -46,6 +48,8 @@ import bisq.network.p2p.SendMailboxMessageListener; import bisq.common.UserThread; import bisq.common.app.Version; +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; import bisq.common.crypto.PubKeyRing; import bisq.common.handlers.FaultHandler; import bisq.common.handlers.ResultHandler; @@ -58,14 +62,18 @@ import org.bitcoinj.utils.Fiat; import javafx.beans.property.IntegerProperty; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import java.security.KeyPair; + import java.util.List; import java.util.Optional; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @@ -81,7 +89,15 @@ public abstract class DisputeManager disputeListService; + private final Config config; private final PriceFeedService priceFeedService; + protected final DaoFacade daoFacade; + + @Getter + protected final ObservableList validationExceptions = + FXCollections.observableArrayList(); + @Getter + private final KeyPair signatureKeyPair; /////////////////////////////////////////////////////////////////////////////////////////// @@ -95,8 +111,10 @@ public abstract class DisputeManager disputeListService, + Config config, PriceFeedService priceFeedService) { super(p2PService, walletsSetup); @@ -105,8 +123,11 @@ public abstract class DisputeManager disputes = getDisputeList().getList(); + disputes.forEach(dispute -> { + try { + TradeDataValidation.validateDonationAddress(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade); + TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config); + TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config); + } catch (TradeDataValidation.AddressException | TradeDataValidation.NodeAddressException e) { + log.error(e.toString()); + validationExceptions.add(e); + } + }); + + TradeDataValidation.testIfAnyDisputeTriedReplay(disputes, + disputeReplayException -> { + log.error(disputeReplayException.toString()); + validationExceptions.add(disputeReplayException); + }); } public boolean isTrader(Dispute dispute) { @@ -308,9 +347,21 @@ public abstract class DisputeManager optionalTrade = tradeManager.getTradeById(dispute.getTradeId()); + if (!optionalTrade.isPresent()) { + return; + } + + Trade trade = optionalTrade.get(); + try { + TradeDataValidation.validatePayoutTx(trade, + trade.getDelayedPayoutTx(), + dispute, + daoFacade, + btcWalletService); + } catch (TradeDataValidation.ValidationException e) { + // The peer sent us an invalid donation address. We do not return here as we don't want to break + // mediation/arbitration and log only the issue. The dispute agent will run validation as well and will get + // a popup displayed to react. + log.error("Donation address invalid. {}", e.toString()); + } + 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())); + trade.setDisputeState(getDisputeStateStartedByPeer()); errorMessage = null; } else { // valid case if both have opened a dispute and agent was not online. @@ -516,6 +586,7 @@ public abstract class DisputeManager storedDisputeOptional = findDispute(dispute); @@ -609,7 +680,7 @@ public abstract class DisputeManager TradeManager tradeManager, ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, - PubKeyRing pubKeyRing, + DaoFacade daoFacade, + KeyRing keyRing, MediationDisputeListService mediationDisputeListService, + Config config, PriceFeedService priceFeedService) { super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, - openOfferManager, pubKeyRing, mediationDisputeListService, priceFeedService); + openOfferManager, daoFacade, keyRing, mediationDisputeListService, config, priceFeedService); } + /////////////////////////////////////////////////////////////////////////////////////////// // Implement template methods /////////////////////////////////////////////////////////////////////////////////////////// @@ -117,7 +122,7 @@ public final class MediationManager extends DisputeManager } @Override - protected Trade.DisputeState getDisputeState_StartedByPeer() { + protected Trade.DisputeState getDisputeStateStartedByPeer() { return Trade.DisputeState.MEDIATION_STARTED_BY_PEER; } diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index f28208f050..25e6b18275 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -20,6 +20,7 @@ package bisq.core.support.dispute.refund; import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.dao.DaoFacade; import bisq.core.locale.Res; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; @@ -44,7 +45,8 @@ import bisq.network.p2p.P2PService; import bisq.common.Timer; import bisq.common.UserThread; import bisq.common.app.Version; -import bisq.common.crypto.PubKeyRing; +import bisq.common.config.Config; +import bisq.common.crypto.KeyRing; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -74,13 +76,16 @@ public final class RefundManager extends DisputeManager { TradeManager tradeManager, ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, - PubKeyRing pubKeyRing, + DaoFacade daoFacade, + KeyRing keyRing, RefundDisputeListService refundDisputeListService, + Config config, PriceFeedService priceFeedService) { super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, - openOfferManager, pubKeyRing, refundDisputeListService, priceFeedService); + openOfferManager, daoFacade, keyRing, refundDisputeListService, config, priceFeedService); } + /////////////////////////////////////////////////////////////////////////////////////////// // Implement template methods /////////////////////////////////////////////////////////////////////////////////////////// @@ -111,7 +116,7 @@ public final class RefundManager extends DisputeManager { } @Override - protected Trade.DisputeState getDisputeState_StartedByPeer() { + protected Trade.DisputeState getDisputeStateStartedByPeer() { return Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER; } diff --git a/core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java b/core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java index 629d7a3129..e69de29bb2 100644 --- a/core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java +++ b/core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java @@ -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.btc.wallet.BtcWalletService; -import bisq.core.dao.DaoFacade; -import bisq.core.dao.governance.param.Param; -import bisq.core.offer.Offer; - -import bisq.common.config.Config; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionInput; -import org.bitcoinj.core.TransactionOutPoint; -import org.bitcoinj.core.TransactionOutput; - -import java.util.List; - -import lombok.extern.slf4j.Slf4j; - -import static com.google.common.base.Preconditions.checkNotNull; - -@Slf4j -public class DelayedPayoutTxValidation { - - public static class DonationAddressException extends Exception { - DonationAddressException(String msg) { - super(msg); - } - } - - public static class MissingDelayedPayoutTxException extends Exception { - MissingDelayedPayoutTxException(String msg) { - super(msg); - } - } - - public static class InvalidTxException extends Exception { - InvalidTxException(String msg) { - super(msg); - } - } - - public static class AmountMismatchException extends Exception { - AmountMismatchException(String msg) { - super(msg); - } - } - - public static class InvalidLockTimeException extends Exception { - InvalidLockTimeException(String msg) { - super(msg); - } - } - - public static class InvalidInputException extends Exception { - InvalidInputException(String msg) { - super(msg); - } - } - - public static void validatePayoutTx(Trade trade, - Transaction delayedPayoutTx, - DaoFacade daoFacade, - BtcWalletService btcWalletService) - throws DonationAddressException, MissingDelayedPayoutTxException, - InvalidTxException, InvalidLockTimeException, AmountMismatchException { - String errorMsg; - if (delayedPayoutTx == null) { - errorMsg = "DelayedPayoutTx must not be null. tradeId=" + trade.getShortId(); - log.error(errorMsg); - throw new MissingDelayedPayoutTxException(errorMsg); - } - - // Validate tx structure - if (delayedPayoutTx.getInputs().size() != 1) { - errorMsg = "Number of delayedPayoutTx inputs must be 1. tradeId=" + trade.getShortId(); - log.error(errorMsg); - log.error(delayedPayoutTx.toString()); - throw new InvalidTxException(errorMsg); - } - - if (delayedPayoutTx.getOutputs().size() != 1) { - errorMsg = "Number of delayedPayoutTx outputs must be 1. tradeId=" + trade.getShortId(); - log.error(errorMsg); - log.error(delayedPayoutTx.toString()); - throw new InvalidTxException(errorMsg); - } - - // connectedOutput is null and input.getValue() is null at that point as the tx is not committed to the wallet - // yet. So we cannot check that the input matches but we did the amount check earlier in the trade protocol. - - // Validate lock time - if (delayedPayoutTx.getLockTime() != trade.getLockTime()) { - errorMsg = "delayedPayoutTx.getLockTime() must match trade.getLockTime(). tradeId=" + trade.getShortId(); - log.error(errorMsg); - log.error(delayedPayoutTx.toString()); - throw new InvalidLockTimeException(errorMsg); - } - - // Validate seq num - if (delayedPayoutTx.getInput(0).getSequenceNumber() != TransactionInput.NO_SEQUENCE - 1) { - errorMsg = "Sequence number must be 0xFFFFFFFE. tradeId=" + trade.getShortId(); - log.error(errorMsg); - log.error(delayedPayoutTx.toString()); - throw new InvalidLockTimeException(errorMsg); - } - - // Check amount - TransactionOutput output = delayedPayoutTx.getOutput(0); - Offer offer = checkNotNull(trade.getOffer()); - Coin msOutputAmount = offer.getBuyerSecurityDeposit() - .add(offer.getSellerSecurityDeposit()) - .add(checkNotNull(trade.getTradeAmount())); - - if (!output.getValue().equals(msOutputAmount)) { - errorMsg = "Output value of deposit tx and delayed payout tx is not matching. Output: " + output + - " / msOutputAmount: " + msOutputAmount + ". tradeId=" + trade.getShortId(); - log.error(errorMsg); - log.error(delayedPayoutTx.toString()); - throw new AmountMismatchException(errorMsg); - } - - - // Validate donation address - // Get most recent donation address. - // We do not support past DAO param addresses to avoid that those receive funds (no bond set up anymore). - // Users who have not synced the DAO cannot trade. - - - NetworkParameters params = btcWalletService.getParams(); - - //TODO update to BitcoinJ API changes - Address address = output.getAddressFromP2PKHScript(params); - - if (address == null) { - // The donation address can be as well be a multisig address. - //TODO update to BitcoinJ API changes - address = output.getAddressFromP2SH(params); - if (address == null) { - errorMsg = "Donation address cannot be resolved (not of type P2PKHScript or P2SH). Output: " + output; - log.error(errorMsg); - log.error(delayedPayoutTx.toString()); - throw new DonationAddressException(errorMsg); - } - } - - String addressAsString = address.toString(); - - // In case the seller has deactivated the DAO the default address will be used. - String defaultDonationAddressString = Param.RECIPIENT_BTC_ADDRESS.getDefaultValue(); - boolean defaultNotMatching = !defaultDonationAddressString.equals(addressAsString); - String recentDonationAddressString = daoFacade.getParamValue(Param.RECIPIENT_BTC_ADDRESS); - boolean recentFromDaoNotMatching = !recentDonationAddressString.equals(addressAsString); - - // If buyer has DAO deactivated or not synced he will not be able to see recent address used by the seller, so - // we add it hard coded here. We need to support also the default one as - // FIXME This is a quick fix and should be improved in future. - // We use the default addresses for non mainnet networks. For dev testing it need to be changed here. - // We use a list to gain more flexibility at updates of DAO param, but still might fail if buyer has not updated - // software. Needs a better solution.... - List hardCodedAddresses = Config.baseCurrencyNetwork().isMainnet() ? - List.of("3EtUWqsGThPtjwUczw27YCo6EWvQdaPUyp", "3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV") : // mainnet - Config.baseCurrencyNetwork().isDaoBetaNet() ? List.of("1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7") : // daoBetaNet - Config.baseCurrencyNetwork().isTestnet() ? List.of("2N4mVTpUZAnhm9phnxB7VrHB4aBhnWrcUrV") : // testnet - List.of("2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w"); // regtest or DAO testnet (regtest) - - boolean noneOfHardCodedMatching = hardCodedAddresses.stream().noneMatch(e -> e.equals(addressAsString)); - - // If seller has DAO deactivated as well we get default address - if (recentFromDaoNotMatching && defaultNotMatching && noneOfHardCodedMatching) { - errorMsg = "Donation address is invalid." + - "\nAddress used by BTC seller: " + addressAsString + - "\nRecent donation address:" + recentDonationAddressString + - "\nDefault donation address: " + defaultDonationAddressString; - log.error(errorMsg); - log.error(delayedPayoutTx.toString()); - throw new DonationAddressException(errorMsg); - } - } - - public static void validatePayoutTxInput(Transaction depositTx, - Transaction delayedPayoutTx) - throws InvalidInputException { - TransactionInput input = delayedPayoutTx.getInput(0); - checkNotNull(input, "delayedPayoutTx.getInput(0) must not be null"); - // input.getConnectedOutput() is null as the tx is not committed at that point - - TransactionOutPoint outpoint = input.getOutpoint(); - if (!outpoint.getHash().toString().equals(depositTx.getTxId().toString()) || outpoint.getIndex() != 0) { - throw new InvalidInputException("Input of delayed payout transaction does not point to output of deposit tx.\n" + - "Delayed payout tx=" + delayedPayoutTx + "\n" + - "Deposit tx=" + depositTx); - } - } -} diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index 58ac174488..9ca0a4f6a1 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -745,9 +745,19 @@ public abstract class Trade implements Tradable, Model { @Nullable public Transaction getDelayedPayoutTx() { if (delayedPayoutTx == null) { - delayedPayoutTx = delayedPayoutTxBytes != null && processModel.getBtcWalletService() != null ? - processModel.getBtcWalletService().getTxFromSerializedTx(delayedPayoutTxBytes) : - null; + BtcWalletService btcWalletService = processModel.getBtcWalletService(); + if (btcWalletService == null) { + log.warn("btcWalletService is null. You might call that method before the tradeManager has " + + "initialized all trades"); + return null; + } + + if (delayedPayoutTxBytes == null) { + log.warn("delayedPayoutTxBytes are null"); + return null; + } + + delayedPayoutTx = btcWalletService.getTxFromSerializedTx(delayedPayoutTxBytes); } return delayedPayoutTx; } diff --git a/core/src/main/java/bisq/core/trade/TradeDataValidation.java b/core/src/main/java/bisq/core/trade/TradeDataValidation.java new file mode 100644 index 0000000000..eea5799225 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/TradeDataValidation.java @@ -0,0 +1,406 @@ +/* + * 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.btc.wallet.BtcWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.offer.Offer; +import bisq.core.support.dispute.Dispute; +import bisq.core.util.validation.RegexValidatorFactory; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.config.Config; +import bisq.common.util.Tuple3; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutPoint; +import org.bitcoinj.core.TransactionOutput; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class TradeDataValidation { + + public static void validateDonationAddress(String addressAsString, DaoFacade daoFacade) + throws AddressException { + validateDonationAddress(null, addressAsString, daoFacade); + } + + public static void validateNodeAddress(Dispute dispute, NodeAddress nodeAddress, Config config) + throws NodeAddressException { + if (!config.useLocalhostForP2P && !RegexValidatorFactory.onionAddressRegexValidator().validate(nodeAddress.getFullAddress()).isValid) { + String msg = "Node address " + nodeAddress.getFullAddress() + " at dispute with trade ID " + + dispute.getShortTradeId() + " is not a valid address"; + log.error(msg); + throw new NodeAddressException(dispute, msg); + } + } + + public static void validateDonationAddress(@Nullable Dispute dispute, String addressAsString, DaoFacade daoFacade) + throws AddressException { + + if (addressAsString == null) { + log.debug("address is null at validateDonationAddress. This is expected in case of an not updated trader."); + return; + } + + Set allPastParamValues = daoFacade.getAllDonationAddresses(); + if (!allPastParamValues.contains(addressAsString)) { + String errorMsg = "Donation address is not a valid DAO donation address." + + "\nAddress used in the dispute: " + addressAsString + + "\nAll DAO param donation addresses:" + allPastParamValues; + log.error(errorMsg); + throw new AddressException(dispute, errorMsg); + } + } + + public static void testIfAnyDisputeTriedReplay(List disputeList, + Consumer exceptionHandler) { + var tuple = getTestReplayHashMaps(disputeList); + Map> disputesPerTradeId = tuple.first; + Map> disputesPerDelayedPayoutTxId = tuple.second; + Map> disputesPerDepositTxId = tuple.third; + + disputeList.forEach(disputeToTest -> { + try { + testIfDisputeTriesReplay(disputeToTest, + disputesPerTradeId, + disputesPerDelayedPayoutTxId, + disputesPerDepositTxId); + + } catch (DisputeReplayException e) { + exceptionHandler.accept(e); + } + }); + } + + + public static void testIfDisputeTriesReplay(Dispute dispute, + List disputeList) throws DisputeReplayException { + var tuple = TradeDataValidation.getTestReplayHashMaps(disputeList); + Map> disputesPerTradeId = tuple.first; + Map> disputesPerDelayedPayoutTxId = tuple.second; + Map> disputesPerDepositTxId = tuple.third; + + testIfDisputeTriesReplay(dispute, + disputesPerTradeId, + disputesPerDelayedPayoutTxId, + disputesPerDepositTxId); + } + + + private static Tuple3>, Map>, Map>> getTestReplayHashMaps( + List disputeList) { + Map> disputesPerTradeId = new HashMap<>(); + Map> disputesPerDelayedPayoutTxId = new HashMap<>(); + Map> disputesPerDepositTxId = new HashMap<>(); + disputeList.forEach(dispute -> { + String uid = dispute.getUid(); + + String tradeId = dispute.getTradeId(); + disputesPerTradeId.putIfAbsent(tradeId, new HashSet<>()); + Set set = disputesPerTradeId.get(tradeId); + set.add(uid); + + String delayedPayoutTxId = dispute.getDelayedPayoutTxId(); + disputesPerDelayedPayoutTxId.putIfAbsent(delayedPayoutTxId, new HashSet<>()); + set = disputesPerDelayedPayoutTxId.get(delayedPayoutTxId); + set.add(uid); + + String depositTxId = dispute.getDepositTxId(); + disputesPerDepositTxId.putIfAbsent(depositTxId, new HashSet<>()); + set = disputesPerDepositTxId.get(depositTxId); + set.add(uid); + }); + + return new Tuple3<>(disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId); + } + + private static void testIfDisputeTriesReplay(Dispute disputeToTest, + Map> disputesPerTradeId, + Map> disputesPerDelayedPayoutTxId, + Map> disputesPerDepositTxId) + throws DisputeReplayException { + + try { + String disputeToTestTradeId = disputeToTest.getTradeId(); + String disputeToTestDelayedPayoutTxId = disputeToTest.getDelayedPayoutTxId(); + String disputeToTestDepositTxId = disputeToTest.getDepositTxId(); + String disputeToTestUid = disputeToTest.getUid(); + + checkNotNull(disputeToTestDelayedPayoutTxId, + "delayedPayoutTxId must not be null. Trade ID: " + disputeToTestTradeId); + checkNotNull(disputeToTestDepositTxId, + "depositTxId must not be null. Trade ID: " + disputeToTestTradeId); + checkNotNull(disputeToTestUid, + "agentsUid must not be null. Trade ID: " + disputeToTestTradeId); + + checkArgument(disputesPerTradeId.get(disputeToTestTradeId).size() <= 2, + "We found more then 2 disputes with the same trade ID. " + + "Trade ID: " + disputeToTestTradeId); + checkArgument(disputesPerDelayedPayoutTxId.get(disputeToTestDelayedPayoutTxId).size() <= 2, + "We found more then 2 disputes with the same delayedPayoutTxId. " + + "Trade ID: " + disputeToTestTradeId); + checkArgument(disputesPerDepositTxId.get(disputeToTestDepositTxId).size() <= 2, + "We found more then 2 disputes with the same depositTxId. " + + "Trade ID: " + disputeToTestTradeId); + + } catch (IllegalArgumentException | NullPointerException e) { + throw new DisputeReplayException(disputeToTest, e.getMessage()); + } + } + + public static void validatePayoutTx(Trade trade, + Transaction delayedPayoutTx, + DaoFacade daoFacade, + BtcWalletService btcWalletService) + throws AddressException, MissingTxException, + InvalidTxException, InvalidLockTimeException, InvalidAmountException { + validatePayoutTx(trade, + delayedPayoutTx, + null, + daoFacade, + btcWalletService, + null); + } + + public static void validatePayoutTx(Trade trade, + Transaction delayedPayoutTx, + @Nullable Dispute dispute, + DaoFacade daoFacade, + BtcWalletService btcWalletService) + throws AddressException, MissingTxException, + InvalidTxException, InvalidLockTimeException, InvalidAmountException { + validatePayoutTx(trade, + delayedPayoutTx, + dispute, + daoFacade, + btcWalletService, + null); + } + + public static void validatePayoutTx(Trade trade, + Transaction delayedPayoutTx, + DaoFacade daoFacade, + BtcWalletService btcWalletService, + @Nullable Consumer addressConsumer) + throws AddressException, MissingTxException, + InvalidTxException, InvalidLockTimeException, InvalidAmountException { + validatePayoutTx(trade, + delayedPayoutTx, + null, + daoFacade, + btcWalletService, + addressConsumer); + } + + public static void validatePayoutTx(Trade trade, + Transaction delayedPayoutTx, + @Nullable Dispute dispute, + DaoFacade daoFacade, + BtcWalletService btcWalletService, + @Nullable Consumer addressConsumer) + throws AddressException, MissingTxException, + InvalidTxException, InvalidLockTimeException, InvalidAmountException { + String errorMsg; + if (delayedPayoutTx == null) { + errorMsg = "DelayedPayoutTx must not be null"; + log.error(errorMsg); + throw new MissingTxException("DelayedPayoutTx must not be null"); + } + + // Validate tx structure + if (delayedPayoutTx.getInputs().size() != 1) { + errorMsg = "Number of delayedPayoutTx inputs must be 1"; + log.error(errorMsg); + log.error(delayedPayoutTx.toString()); + throw new InvalidTxException(errorMsg); + } + + if (delayedPayoutTx.getOutputs().size() != 1) { + errorMsg = "Number of delayedPayoutTx outputs must be 1"; + log.error(errorMsg); + log.error(delayedPayoutTx.toString()); + throw new InvalidTxException(errorMsg); + } + + // connectedOutput is null and input.getValue() is null at that point as the tx is not committed to the wallet + // yet. So we cannot check that the input matches but we did the amount check earlier in the trade protocol. + + // Validate lock time + if (delayedPayoutTx.getLockTime() != trade.getLockTime()) { + errorMsg = "delayedPayoutTx.getLockTime() must match trade.getLockTime()"; + log.error(errorMsg); + log.error(delayedPayoutTx.toString()); + throw new InvalidLockTimeException(errorMsg); + } + + // Validate seq num + if (delayedPayoutTx.getInput(0).getSequenceNumber() != TransactionInput.NO_SEQUENCE - 1) { + errorMsg = "Sequence number must be 0xFFFFFFFE"; + log.error(errorMsg); + log.error(delayedPayoutTx.toString()); + throw new InvalidLockTimeException(errorMsg); + } + + // Check amount + TransactionOutput output = delayedPayoutTx.getOutput(0); + Offer offer = checkNotNull(trade.getOffer()); + Coin msOutputAmount = offer.getBuyerSecurityDeposit() + .add(offer.getSellerSecurityDeposit()) + .add(checkNotNull(trade.getTradeAmount())); + + if (!output.getValue().equals(msOutputAmount)) { + errorMsg = "Output value of deposit tx and delayed payout tx is not matching. Output: " + output + " / msOutputAmount: " + msOutputAmount; + log.error(errorMsg); + log.error(delayedPayoutTx.toString()); + throw new InvalidAmountException(errorMsg); + } + + NetworkParameters params = btcWalletService.getParams(); + Address address = output.getAddressFromP2PKHScript(params); + if (address == null) { + // The donation address can be a multisig address as well. + address = output.getAddressFromP2SH(params); + if (address == null) { + errorMsg = "Donation address cannot be resolved (not of type P2PKHScript or P2SH). Output: " + output; + log.error(errorMsg); + log.error(delayedPayoutTx.toString()); + throw new AddressException(dispute, errorMsg); + } + } + + String addressAsString = address.toString(); + if (addressConsumer != null) { + addressConsumer.accept(addressAsString); + } + + validateDonationAddress(addressAsString, daoFacade); + + if (dispute != null) { + // Verify that address in the dispute matches the one in the trade. + String donationAddressOfDelayedPayoutTx = dispute.getDonationAddressOfDelayedPayoutTx(); + // Old clients don't have it set yet. Can be removed after a forced update + if (donationAddressOfDelayedPayoutTx != null) { + checkArgument(addressAsString.equals(donationAddressOfDelayedPayoutTx), + "donationAddressOfDelayedPayoutTx from dispute does not match address from delayed payout tx"); + } + } + } + + public static void validatePayoutTxInput(Transaction depositTx, + Transaction delayedPayoutTx) + throws InvalidInputException { + TransactionInput input = delayedPayoutTx.getInput(0); + checkNotNull(input, "delayedPayoutTx.getInput(0) must not be null"); + // input.getConnectedOutput() is null as the tx is not committed at that point + + TransactionOutPoint outpoint = input.getOutpoint(); + if (!outpoint.getHash().toString().equals(depositTx.getTxId().toString()) || outpoint.getIndex() != 0) { + throw new InvalidInputException("Input of delayed payout transaction does not point to output of deposit tx.\n" + + "Delayed payout tx=" + delayedPayoutTx + "\n" + + "Deposit tx=" + depositTx); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Exceptions + /////////////////////////////////////////////////////////////////////////////////////////// + + public static class ValidationException extends Exception { + @Nullable + @Getter + private final Dispute dispute; + + ValidationException(String msg) { + this(null, msg); + } + + ValidationException(@Nullable Dispute dispute, String msg) { + super(msg); + this.dispute = dispute; + } + } + + public static class AddressException extends ValidationException { + AddressException(@Nullable Dispute dispute, String msg) { + super(dispute, msg); + } + } + + public static class MissingTxException extends ValidationException { + MissingTxException(String msg) { + super(msg); + } + } + + public static class InvalidTxException extends ValidationException { + InvalidTxException(String msg) { + super(msg); + } + } + + public static class InvalidAmountException extends ValidationException { + InvalidAmountException(String msg) { + super(msg); + } + } + + public static class InvalidLockTimeException extends ValidationException { + InvalidLockTimeException(String msg) { + super(msg); + } + } + + public static class InvalidInputException extends ValidationException { + InvalidInputException(String msg) { + super(msg); + } + } + + public static class DisputeReplayException extends ValidationException { + DisputeReplayException(Dispute dispute, String msg) { + super(dispute, msg); + } + } + + public static class NodeAddressException extends ValidationException { + NodeAddressException(Dispute dispute, String msg) { + super(dispute, msg); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 018b1200ac..0da5739767 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -135,6 +135,7 @@ public class TradeManager implements PersistedDataHost { private final Storage> tradableListStorage; private TradableList tradableList; + @Getter private final BooleanProperty pendingTradesInitialized = new SimpleBooleanProperty(); private List tradesForStatistics; @Setter @@ -305,15 +306,11 @@ public class TradeManager implements PersistedDataHost { } try { - DelayedPayoutTxValidation.validatePayoutTx(trade, + TradeDataValidation.validatePayoutTx(trade, trade.getDelayedPayoutTx(), daoFacade, btcWalletService); - } catch (DelayedPayoutTxValidation.DonationAddressException | - DelayedPayoutTxValidation.InvalidTxException | - DelayedPayoutTxValidation.InvalidLockTimeException | - DelayedPayoutTxValidation.MissingDelayedPayoutTxException | - DelayedPayoutTxValidation.AmountMismatchException e) { + } catch (TradeDataValidation.ValidationException e) { log.warn("Delayed payout tx exception, trade {}, exception {}", trade.getId(), e.getMessage()); if (!allowFaultyDelayedTxs) { // We move it to failed trades so it cannot be continued. diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java index c00bd19861..ec8c468424 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java @@ -17,8 +17,8 @@ package bisq.core.trade.protocol.tasks.buyer; -import bisq.core.trade.DelayedPayoutTxValidation; import bisq.core.trade.Trade; +import bisq.core.trade.TradeDataValidation; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.common.taskrunner.TaskRunner; @@ -40,23 +40,21 @@ public class BuyerVerifiesFinalDelayedPayoutTx extends TradeTask { try { runInterceptHook(); - Transaction delayedPayoutTx = checkNotNull(trade.getDelayedPayoutTx()); - DelayedPayoutTxValidation.validatePayoutTx(trade, + Transaction delayedPayoutTx = trade.getDelayedPayoutTx(); + checkNotNull(delayedPayoutTx, "trade.getDelayedPayoutTx() must not be null"); + // Check again tx + TradeDataValidation.validatePayoutTx(trade, delayedPayoutTx, processModel.getDaoFacade(), processModel.getBtcWalletService()); // Now as we know the deposit tx we can also verify the input - Transaction depositTx = checkNotNull(trade.getDepositTx()); - DelayedPayoutTxValidation.validatePayoutTxInput(depositTx, delayedPayoutTx); + Transaction depositTx = trade.getDepositTx(); + checkNotNull(depositTx, "trade.getDepositTx() must not be null"); + TradeDataValidation.validatePayoutTxInput(depositTx, delayedPayoutTx); complete(); - } catch (DelayedPayoutTxValidation.DonationAddressException | - DelayedPayoutTxValidation.MissingDelayedPayoutTxException | - DelayedPayoutTxValidation.InvalidTxException | - DelayedPayoutTxValidation.InvalidLockTimeException | - DelayedPayoutTxValidation.AmountMismatchException | - DelayedPayoutTxValidation.InvalidInputException e) { + } catch (TradeDataValidation.ValidationException e) { failed(e.getMessage()); } catch (Throwable t) { failed(t); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java index 37c4388a79..270713dbe9 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java @@ -17,8 +17,8 @@ package bisq.core.trade.protocol.tasks.buyer; -import bisq.core.trade.DelayedPayoutTxValidation; import bisq.core.trade.Trade; +import bisq.core.trade.TradeDataValidation; import bisq.core.trade.protocol.tasks.TradeTask; import bisq.common.taskrunner.TaskRunner; @@ -36,17 +36,13 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask { try { runInterceptHook(); - DelayedPayoutTxValidation.validatePayoutTx(trade, + TradeDataValidation.validatePayoutTx(trade, processModel.getPreparedDelayedPayoutTx(), processModel.getDaoFacade(), processModel.getBtcWalletService()); complete(); - } catch (DelayedPayoutTxValidation.DonationAddressException | - DelayedPayoutTxValidation.MissingDelayedPayoutTxException | - DelayedPayoutTxValidation.InvalidTxException | - DelayedPayoutTxValidation.InvalidLockTimeException | - DelayedPayoutTxValidation.AmountMismatchException e) { + } catch (TradeDataValidation.ValidationException e) { failed(e.getMessage()); } catch (Throwable t) { failed(t); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 06bb9868b6..d6084147c9 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -214,7 +214,8 @@ shared.mediator=Mediator shared.arbitrator=Arbitrator shared.refundAgent=Arbitrator shared.refundAgentForSupportStaff=Refund agent -shared.delayedPayoutTxId=Refund collateral transaction ID +shared.delayedPayoutTxId=Delayed payout transaction ID +shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. @@ -894,6 +895,12 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll (or if the other peer is unresponsive).\n\n\ More details about the new arbitration model:\n\ https://docs.bisq.network/trading-rules.html#arbitration +portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout \ + but it seems that your trading peer has not accepted it.\n\n\ + The lock time is since {0} (block {1}) over and you can open a second-round dispute with an arbitrator who will \ + investigate the case again and do a payout based on their findings.\n\n\ + You can find more details about the arbitration model at:\n\ + https://docs.bisq.network/trading-rules.html#arbitration portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted @@ -1007,11 +1014,24 @@ support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets support.filter=Search disputes support.filter.prompt=Enter trade ID, date, onion address or account data + +support.sigCheck.button=Verify result +support.sigCheck.popup.info=In case of a reimbursement request to the DAO you need to paste the summary message of the \ + mediation and arbitration process in your reimbursement request on Github. To make this statement verifiable any user can \ + check with this tool if the signature of the mediator or arbitrator matches the summary message. +support.sigCheck.popup.header=Verify dispute result signature +support.sigCheck.popup.msg.label=Summary message +support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute +support.sigCheck.popup.result=Validation result +support.sigCheck.popup.success=Signature is valid +support.sigCheck.popup.failed=Signature verification failed +support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute. + support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? -support.reOpenButton.label=Re-open dispute -support.sendNotificationButton.label=Send private notification -support.reportButton.label=Generate report -support.fullReportButton.label=Get text dump of all disputes +support.reOpenButton.label=Re-open +support.sendNotificationButton.label=Private notification +support.reportButton.label=Report +support.fullReportButton.label=All disputes support.noTickets=There are no open tickets support.sendingMessage=Sending Message... support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. @@ -1081,6 +1101,15 @@ support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nB support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsAddress=Mediator''s node address: {0} +support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. \ + It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. \ + Please inform the developers about that incident and do not close that case before the situation is resolved!\n\n\ + Address used in the dispute: {0}\n\n\ + All DAO param donation addresses: {1}\n\n\ + Trade ID: {2}\ + {3} +support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute? +support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout. #################################################################### @@ -2437,15 +2466,25 @@ disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled disputeSummaryWindow.summaryNotes=Summary notes disputeSummaryWindow.addSummaryNotes=Add summary notes disputeSummaryWindow.close.button=Close ticket -disputeSummaryWindow.close.msg=Ticket closed on {0}\n\n\ -Summary:\n\ -Payout amount for BTC buyer: {1}\n\ -Payout amount for BTC seller: {2}\n\n\ -Reason for dispute: {3}\n\n\ -Summary notes:\n{4} -disputeSummaryWindow.close.nextStepsForMediation=\n\nNext steps:\n\ + +# Do no change any line break or order of tokens as the structure is used for signature verification +disputeSummaryWindow.close.msg=Ticket closed on {0}\n\ + {1} node address: {2}\n\n\ + Summary:\n\ + Trade ID: {3}\n\ + Currency: {4}\n\ + Trade amount: {5}\n\ + Payout amount for BTC buyer: {6}\n\ + Payout amount for BTC seller: {7}\n\n\ + Reason for dispute: {8}\n\n\ + Summary notes:\n{9}\n + +# Do no change any line break or order of tokens as the structure is used for signature verification +disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3} + +disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\n\ Open trade and accept or reject suggestion from mediator -disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nNext steps:\n\ +disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\n\ No further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket! disputeSummaryWindow.close.txDetails.headline=Publish refund transaction @@ -2457,6 +2496,9 @@ disputeSummaryWindow.close.txDetails=Spending: {0}\n\ Transaction size: {5} Kb\n\n\ Are you sure you want to publish this transaction? +disputeSummaryWindow.close.noPayout.headline=Close without any payout +disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? + emptyWalletWindow.headline={0} emergency wallet tool emptyWalletWindow.info=Please use that only in emergency case if you cannot access your fund from the UI.\n\n\ Please note that all open offers will be closed automatically when using this tool.\n\n\ 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 ff053d9033..ec8e10ede0 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 @@ -141,6 +141,8 @@ public class ContractWindow extends Overlay { rows++; if (dispute.getDelayedPayoutTxId() != null) rows++; + if (dispute.getDonationAddressOfDelayedPayoutTx() != null) + rows++; if (showAcceptedCountryCodes) rows++; if (showAcceptedBanks) @@ -248,6 +250,11 @@ public class ContractWindow extends Overlay { if (dispute.getDelayedPayoutTxId() != null) addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxId"), dispute.getDelayedPayoutTxId()); + if (dispute.getDonationAddressOfDelayedPayoutTx() != null) { + addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxReceiverAddress"), + dispute.getDonationAddressOfDelayedPayoutTx()); + } + if (dispute.getPayoutTxSerialized() != null) addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.payoutTxId"), dispute.getPayoutTxId()); 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 6202b6b204..82fd1c09f3 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 @@ -23,6 +23,7 @@ import bisq.desktop.components.BisqTextArea; import bisq.desktop.components.InputTextField; import bisq.desktop.main.overlays.Overlay; import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.support.dispute.DisputeSummaryVerification; import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.Layout; @@ -34,6 +35,7 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.Restrictions; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.dao.DaoFacade; import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.provider.fee.FeeService; @@ -45,6 +47,7 @@ import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.Contract; +import bisq.core.trade.TradeDataValidation; import bisq.core.util.FormattingUtils; import bisq.core.util.ParsingUtils; import bisq.core.util.coin.CoinFormatter; @@ -86,8 +89,7 @@ import java.util.Date; import java.util.Optional; import java.util.concurrent.TimeUnit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import lombok.extern.slf4j.Slf4j; import static bisq.desktop.util.FormBuilder.add2ButtonsWithBox; import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel; @@ -95,9 +97,8 @@ import static bisq.desktop.util.FormBuilder.addTitledGroupBg; import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox; import static com.google.common.base.Preconditions.checkNotNull; +@Slf4j public class DisputeSummaryWindow extends Overlay { - private static final Logger log = LoggerFactory.getLogger(DisputeSummaryWindow.class); - private final CoinFormatter formatter; private final MediationManager mediationManager; private final RefundManager refundManager; @@ -105,8 +106,9 @@ public class DisputeSummaryWindow extends Overlay { private final BtcWalletService btcWalletService; private final TxFeeEstimationService txFeeEstimationService; private final FeeService feeService; + private final DaoFacade daoFacade; private Dispute dispute; - private Optional finalizeDisputeHandlerOptional = Optional.empty(); + private Optional finalizeDisputeHandlerOptional = Optional.empty(); private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup; private DisputeResult disputeResult; private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton, @@ -141,7 +143,8 @@ public class DisputeSummaryWindow extends Overlay { TradeWalletService tradeWalletService, BtcWalletService btcWalletService, TxFeeEstimationService txFeeEstimationService, - FeeService feeService) { + FeeService feeService, + DaoFacade daoFacade) { this.formatter = formatter; this.mediationManager = mediationManager; @@ -150,6 +153,7 @@ public class DisputeSummaryWindow extends Overlay { this.btcWalletService = btcWalletService; this.txFeeEstimationService = txFeeEstimationService; this.feeService = feeService; + this.daoFacade = daoFacade; type = Type.Confirmation; } @@ -222,7 +226,7 @@ public class DisputeSummaryWindow extends Overlay { else disputeResult = dispute.getDisputeResultProperty().get(); - peersDisputeOptional = getDisputeManager(dispute).getDisputesAsObservableList().stream() + peersDisputeOptional = checkNotNull(getDisputeManager(dispute)).getDisputesAsObservableList().stream() .filter(d -> dispute.getTradeId().equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()) .findFirst(); @@ -382,14 +386,15 @@ public class DisputeSummaryWindow extends Overlay { .add(offer.getSellerSecurityDeposit()); Coin totalAmount = buyerAmount.add(sellerAmount); - if (!totalAmount.isPositive()) { - return false; - } - - if (getDisputeManager(dispute) instanceof RefundManager) { - // We allow to spend less in case of RefundAgent + boolean isRefundAgent = getDisputeManager(dispute) instanceof RefundManager; + if (isRefundAgent) { + // We allow to spend less in case of RefundAgent or even zero to both, so in that case no payout tx will + // be made return totalAmount.compareTo(available) <= 0; } else { + if (!totalAmount.isPositive()) { + return false; + } return totalAmount.compareTo(available) == 0; } } @@ -642,15 +647,15 @@ public class DisputeSummaryWindow extends Overlay { log.warn("dispute.getDepositTxSerialized is null"); return; } + if (dispute.getSupportType() == SupportType.REFUND && peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed()) { - showPayoutTxConfirmation(contract, disputeResult, - () -> { - doClose(closeTicketButton); - }); + showPayoutTxConfirmation(contract, + disputeResult, + () -> doCloseIfValid(closeTicketButton)); } else { - doClose(closeTicketButton); + doCloseIfValid(closeTicketButton); } }); @@ -684,28 +689,36 @@ public class DisputeSummaryWindow extends Overlay { formatter.formatCoinWithCode(sellerPayoutAmount), sellerPayoutAddressString); } - new Popup().width(900) - .headLine(Res.get("disputeSummaryWindow.close.txDetails.headline")) - .confirmation(Res.get("disputeSummaryWindow.close.txDetails", - formatter.formatCoinWithCode(inputAmount), - buyerDetails, - sellerDetails, - formatter.formatCoinWithCode(fee), - feePerByte, - kb)) - .actionButtonText(Res.get("shared.yes")) - .onAction(() -> { - doPayout(buyerPayoutAmount, - sellerPayoutAmount, - fee, - buyerPayoutAddressString, - sellerPayoutAddressString, - resultHandler); - }) - .closeButtonText(Res.get("shared.cancel")) - .onClose(() -> { - }) - .show(); + if (outputAmount.isPositive()) { + new Popup().width(900) + .headLine(Res.get("disputeSummaryWindow.close.txDetails.headline")) + .confirmation(Res.get("disputeSummaryWindow.close.txDetails", + formatter.formatCoinWithCode(inputAmount), + buyerDetails, + sellerDetails, + formatter.formatCoinWithCode(fee), + feePerByte, + kb)) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + doPayout(buyerPayoutAmount, + sellerPayoutAmount, + fee, + buyerPayoutAddressString, + sellerPayoutAddressString, + resultHandler); + }) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } else { + // No payout will be made + new Popup().headLine(Res.get("disputeSummaryWindow.close.noPayout.headline")) + .confirmation(Res.get("disputeSummaryWindow.close.noPayout.text")) + .actionButtonText(Res.get("shared.yes")) + .onAction(resultHandler::handleResult) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } } private void doPayout(Coin buyerPayoutAmount, @@ -720,7 +733,6 @@ public class DisputeSummaryWindow extends Overlay { fee, buyerPayoutAddressString, sellerPayoutAddressString); - log.error("transaction " + tx); tradeWalletService.broadcastTx(tx, new TxBroadcaster.Callback() { @Override public void onSuccess(Transaction transaction) { @@ -731,7 +743,6 @@ public class DisputeSummaryWindow extends Overlay { public void onFailure(TxBroadcastException exception) { log.error("TxBroadcastException at doPayout", exception); new Popup().error(exception.toString()).show(); - ; } }); } catch (InsufficientMoneyException | WalletException | TransactionVerificationException e) { @@ -740,32 +751,105 @@ public class DisputeSummaryWindow extends Overlay { } } + private void doCloseIfValid(Button closeTicketButton) { + var disputeManager = checkNotNull(getDisputeManager(dispute)); + try { + TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade); + TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeManager.getDisputesAsObservableList()); + doClose(closeTicketButton); + } catch (TradeDataValidation.AddressException exception) { + String addressAsString = dispute.getDonationAddressOfDelayedPayoutTx(); + String tradeId = dispute.getTradeId(); + + // For mediators we do not enforce that the case cannot be closed to stay flexible, + // but for refund agents we do. + if (disputeManager instanceof MediationManager) { + new Popup().width(900) + .warning(Res.get("support.warning.disputesWithInvalidDonationAddress", + addressAsString, + daoFacade.getAllDonationAddresses(), + tradeId, + Res.get("support.warning.disputesWithInvalidDonationAddress.mediator"))) + .onAction(() -> { + doClose(closeTicketButton); + }) + .actionButtonText(Res.get("shared.yes")) + .closeButtonText(Res.get("shared.no")) + .show(); + } else { + new Popup().width(900) + .warning(Res.get("support.warning.disputesWithInvalidDonationAddress", + addressAsString, + daoFacade.getAllDonationAddresses(), + tradeId, + Res.get("support.warning.disputesWithInvalidDonationAddress.refundAgent"))) + .show(); + } + } catch (TradeDataValidation.DisputeReplayException exception) { + if (disputeManager instanceof MediationManager) { + new Popup().width(900) + .warning(exception.getMessage()) + .onAction(() -> { + doClose(closeTicketButton); + }) + .actionButtonText(Res.get("shared.yes")) + .closeButtonText(Res.get("shared.no")) + .show(); + } else { + new Popup().width(900) + .warning(exception.getMessage()) + .show(); + } + } + } + private void doClose(Button closeTicketButton) { + DisputeManager> disputeManager = getDisputeManager(dispute); + if (disputeManager == null) { + return; + } + + boolean isRefundAgent = disputeManager instanceof RefundManager; disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected()); disputeResult.setCloseDate(new Date()); dispute.setDisputeResult(disputeResult); dispute.setIsClosed(true); DisputeResult.Reason reason = disputeResult.getReason(); - String text = Res.get("disputeSummaryWindow.close.msg", + + summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty()); + String role = isRefundAgent ? Res.get("shared.refundAgent") : Res.get("shared.mediator"); + String agentNodeAddress = checkNotNull(disputeManager.getAgentNodeAddress(dispute)).getFullAddress(); + Contract contract = dispute.getContract(); + String currencyCode = contract.getOfferPayload().getCurrencyCode(); + String amount = formatter.formatCoinWithCode(contract.getTradeAmount()); + String textToSign = Res.get("disputeSummaryWindow.close.msg", DisplayUtils.formatDateTime(disputeResult.getCloseDate()), + role, + agentNodeAddress, + dispute.getShortTradeId(), + currencyCode, + amount, formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()), formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()), Res.get("disputeSummaryWindow.reason." + reason.name()), - disputeResult.summaryNotesProperty().get()); + disputeResult.summaryNotesProperty().get() + ); if (reason == DisputeResult.Reason.OPTION_TRADE && dispute.getChatMessages().size() > 1 && dispute.getChatMessages().get(1).isSystemMessage()) { - text += "\n\n" + dispute.getChatMessages().get(1).getMessage(); + textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n"; } - if (dispute.getSupportType() == SupportType.MEDIATION) { - text += Res.get("disputeSummaryWindow.close.nextStepsForMediation"); - } else if (dispute.getSupportType() == SupportType.REFUND) { - text += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration"); + String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign); + + if (isRefundAgent) { + summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration"); + } else { + summaryText += Res.get("disputeSummaryWindow.close.nextStepsForMediation"); } - checkNotNull(getDisputeManager(dispute)).sendDisputeResultMessage(disputeResult, dispute, text); + disputeManager.sendDisputeResultMessage(disputeResult, dispute, summaryText); if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { UserThread.runAfter(() -> new Popup() diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java new file mode 100644 index 0000000000..9f27499545 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java @@ -0,0 +1,96 @@ +/* + * 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.overlays.windows; + +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.main.support.dispute.DisputeSummaryVerification; + +import bisq.core.locale.Res; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; + +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; + +import javafx.geometry.HPos; +import javafx.geometry.Insets; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.desktop.util.FormBuilder.addMultilineLabel; +import static bisq.desktop.util.FormBuilder.addTopLabelTextArea; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +@Slf4j +public class VerifyDisputeResultSignatureWindow extends Overlay { + private TextArea textArea; + private TextField resultTextField; + private final MediatorManager mediatorManager; + private final RefundAgentManager refundAgentManager; + + public VerifyDisputeResultSignatureWindow(MediatorManager mediatorManager, RefundAgentManager refundAgentManager) { + this.mediatorManager = mediatorManager; + this.refundAgentManager = refundAgentManager; + + type = Type.Attention; + } + + public void show() { + if (headLine == null) + headLine = Res.get("support.sigCheck.popup.header"); + + width = 1050; + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + + applyStyles(); + display(); + + textArea.textProperty().addListener((observable, oldValue, newValue) -> { + resultTextField.setText(DisputeSummaryVerification.verifySignature(newValue, + mediatorManager, + refundAgentManager)); + }); + } + + @Override + protected void createGridPane() { + gridPane = new GridPane(); + gridPane.setHgap(5); + gridPane.setVgap(5); + gridPane.setPadding(new Insets(64, 64, 64, 64)); + gridPane.setPrefWidth(width); + + ColumnConstraints columnConstraints1 = new ColumnConstraints(); + columnConstraints1.setHalignment(HPos.RIGHT); + columnConstraints1.setHgrow(Priority.SOMETIMES); + gridPane.getColumnConstraints().addAll(columnConstraints1); + } + + private void addContent() { + addMultilineLabel(gridPane, ++rowIndex, Res.get("support.sigCheck.popup.info"), 0, width); + textArea = addTopLabelTextArea(gridPane, ++rowIndex, Res.get("support.sigCheck.popup.msg.label"), + Res.get("support.sigCheck.popup.msg.prompt")).second; + resultTextField = addTopLabelTextField(gridPane, ++rowIndex, Res.get("support.sigCheck.popup.result")).second; + } +} 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 2a4e584f79..053fbb0b8d 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 @@ -49,6 +49,7 @@ import bisq.core.support.traderchat.TraderChatManager; import bisq.core.trade.BuyerTrade; import bisq.core.trade.SellerTrade; import bisq.core.trade.Trade; +import bisq.core.trade.TradeDataValidation; import bisq.core.trade.TradeManager; import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; @@ -78,6 +79,8 @@ import javafx.collections.ObservableList; import org.bouncycastle.crypto.params.KeyParameter; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import lombok.Getter; @@ -495,6 +498,28 @@ public class PendingTradesDataModel extends ActivatableDataModel { // In case we re-open a dispute we allow Trade.DisputeState.REFUND_REQUESTED useRefundAgent = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.REFUND_REQUESTED; + AtomicReference donationAddressString = new AtomicReference<>(""); + Transaction delayedPayoutTx = trade.getDelayedPayoutTx(); + try { + TradeDataValidation.validatePayoutTx(trade, + delayedPayoutTx, + daoFacade, + btcWalletService, + donationAddressString::set); + } catch (TradeDataValidation.ValidationException e) { + // The peer sent us an invalid donation address. We do not return here as we don't want to break + // mediation/arbitration and log only the issue. The dispute agent will run validation as well and will get + // a popup displayed to react. + log.error("DelayedPayoutTxValidation failed. {}", e.toString()); + + if (useRefundAgent) { + // We don't allow to continue and publish payout tx and open refund agent case. + // In case it was caused by some bug we want to prevent a wrong payout. In case its a scam attempt we + // want to protect the refund agent. + return; + } + } + ResultHandler resultHandler; if (useMediation) { // If no dispute state set we start with mediation @@ -523,6 +548,11 @@ public class PendingTradesDataModel extends ActivatableDataModel { isSupportTicket, SupportType.MEDIATION); + dispute.setDonationAddressOfDelayedPayoutTx(donationAddressString.get()); + if (delayedPayoutTx != null) { + dispute.setDelayedPayoutTxId(delayedPayoutTx.getTxId().toString()); + } + trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED); disputeManager.sendOpenNewDisputeMessage(dispute, false, @@ -547,7 +577,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { } else if (useRefundAgent) { resultHandler = () -> navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class); - if (trade.getDelayedPayoutTx() == null) { + if (delayedPayoutTx == null) { log.error("Delayed payout tx is missing"); return; } @@ -562,13 +592,12 @@ public class PendingTradesDataModel extends ActivatableDataModel { return; } - long lockTime = trade.getDelayedPayoutTx().getLockTime(); + long lockTime = delayedPayoutTx.getLockTime(); int bestChainHeight = btcWalletService.getBestChainHeight(); long remaining = lockTime - bestChainHeight; if (remaining > 0) { - new Popup() - .instruction(Res.get("portfolio.pending.timeLockNotOver", - FormattingUtils.getDateFromBlockHeight(remaining), remaining)) + new Popup().instruction(Res.get("portfolio.pending.timeLockNotOver", + FormattingUtils.getDateFromBlockHeight(remaining), remaining)) .show(); return; } @@ -611,7 +640,8 @@ public class PendingTradesDataModel extends ActivatableDataModel { } }); - dispute.setDelayedPayoutTxId(trade.getDelayedPayoutTx().getTxId().toString()); + dispute.setDonationAddressOfDelayedPayoutTx(donationAddressString.get()); + dispute.setDelayedPayoutTxId(delayedPayoutTx.getTxId().toString()); trade.setDisputeState(Trade.DisputeState.REFUND_REQUESTED); 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 cfc35e8968..4c91de8614 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 @@ -30,6 +30,7 @@ import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.mediation.MediationResultState; import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.user.Preferences; @@ -41,6 +42,9 @@ import bisq.common.ClockWatcher; import bisq.common.UserThread; import bisq.common.util.Tuple3; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.listeners.NewBestBlockListener; + import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; @@ -62,6 +66,7 @@ import javafx.geometry.Insets; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; +import javafx.beans.property.BooleanProperty; import javafx.beans.value.ChangeListener; import java.util.Optional; @@ -97,6 +102,8 @@ public abstract class TradeStepView extends AnchorPane { private Popup acceptMediationResultPopup; private BootstrapListener bootstrapListener; private TradeSubView.ChatCallback chatCallback; + private final NewBestBlockListener newBestBlockListener; + private ChangeListener pendingTradesInitializedListener; /////////////////////////////////////////////////////////////////////////////////////////// @@ -158,6 +165,10 @@ public abstract class TradeStepView extends AnchorPane { updateTimeLeft(); } }; + + newBestBlockListener = block -> { + checkIfLockTimeIsOver(); + }; } public void activate() { @@ -200,14 +211,34 @@ public abstract class TradeStepView extends AnchorPane { } tradePeriodStateSubscription = EasyBind.subscribe(trade.tradePeriodStateProperty(), newValue -> { - if (newValue != null) + if (newValue != null) { updateTradePeriodState(newValue); + } }); model.clockWatcher.addListener(clockListener); - if (infoLabel != null) + if (infoLabel != null) { infoLabel.setText(getInfoText()); + } + + BooleanProperty pendingTradesInitialized = model.dataModel.tradeManager.getPendingTradesInitialized(); + if (pendingTradesInitialized.get()) { + onPendingTradesInitialized(); + } else { + pendingTradesInitializedListener = (observable, oldValue, newValue) -> { + if (newValue) { + onPendingTradesInitialized(); + UserThread.execute(() -> pendingTradesInitialized.removeListener(pendingTradesInitializedListener)); + } + }; + pendingTradesInitialized.addListener(pendingTradesInitializedListener); + } + } + + protected void onPendingTradesInitialized() { + model.dataModel.btcWalletService.addNewBestBlockListener(newBestBlockListener); + checkIfLockTimeIsOver(); } private void registerSubscriptions() { @@ -262,6 +293,15 @@ public abstract class TradeStepView extends AnchorPane { if (tradeStepInfo != null) tradeStepInfo.setOnAction(null); + + if (newBestBlockListener != null) { + model.dataModel.btcWalletService.removeNewBestBlockListener(newBestBlockListener); + } + + if (acceptMediationResultPopup != null) { + acceptMediationResultPopup.hide(); + acceptMediationResultPopup = null; + } } /////////////////////////////////////////////////////////////////////////////////////////// @@ -445,6 +485,11 @@ public abstract class TradeStepView extends AnchorPane { tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_SELF_REQUESTED); }); + if (acceptMediationResultPopup != null) { + acceptMediationResultPopup.hide(); + acceptMediationResultPopup = null; + } + break; case REFUND_REQUEST_STARTED_BY_PEER: if (tradeStepInfo != null) { @@ -457,6 +502,11 @@ public abstract class TradeStepView extends AnchorPane { if (tradeStepInfo != null) tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED); }); + + if (acceptMediationResultPopup != null) { + acceptMediationResultPopup.hide(); + acceptMediationResultPopup = null; + } break; case REFUND_REQUEST_CLOSED: break; @@ -563,13 +613,34 @@ public abstract class TradeStepView extends AnchorPane { String actionButtonText = hasSelfAccepted() ? Res.get("portfolio.pending.mediationResult.popup.alreadyAccepted") : Res.get("shared.accept"); - acceptMediationResultPopup = new Popup().width(900) - .headLine(headLine) - .instruction(Res.get("portfolio.pending.mediationResult.popup.info", + String message; + MediationResultState mediationResultState = checkNotNull(trade).getMediationResultState(); + if (mediationResultState == null) { + return; + } + + switch (mediationResultState) { + case MEDIATION_RESULT_ACCEPTED: + case SIG_MSG_SENT: + case SIG_MSG_ARRIVED: + case SIG_MSG_IN_MAILBOX: + case SIG_MSG_SEND_FAILED: + message = Res.get("portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver", + FormattingUtils.getDateFromBlockHeight(remaining), + lockTime); + break; + default: + message = Res.get("portfolio.pending.mediationResult.popup.info", myPayoutAmount, peersPayoutAmount, FormattingUtils.getDateFromBlockHeight(remaining), - lockTime)) + lockTime); + break; + } + + acceptMediationResultPopup = new Popup().width(900) + .headLine(headLine) + .instruction(message) .actionButtonText(actionButtonText) .onAction(() -> { model.dataModel.mediationManager.acceptMediationResult(trade, @@ -656,6 +727,18 @@ public abstract class TradeStepView extends AnchorPane { return trade.getDisputeState() != Trade.DisputeState.NO_DISPUTE; } + private void checkIfLockTimeIsOver() { + Transaction delayedPayoutTx = trade.getDelayedPayoutTx(); + if (delayedPayoutTx != null) { + long lockTime = delayedPayoutTx.getLockTime(); + int bestChainHeight = model.dataModel.btcWalletService.getBestChainHeight(); + long remaining = lockTime - bestChainHeight; + if (remaining <= 0) { + openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline", trade.getShortId())); + } + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // TradeDurationLimitInfo 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 b81ebc0f41..cc916cf23d 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 @@ -22,7 +22,7 @@ import bisq.desktop.main.portfolio.pendingtrades.PendingTradesViewModel; import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import bisq.core.locale.Res; -import bisq.core.trade.DelayedPayoutTxValidation; +import bisq.core.trade.TradeDataValidation; public class BuyerStep1View extends TradeStepView { @@ -35,23 +35,9 @@ public class BuyerStep1View extends TradeStepView { } @Override - public void activate() { - super.activate(); - - try { - DelayedPayoutTxValidation.validatePayoutTx(trade, - trade.getDelayedPayoutTx(), - model.dataModel.daoFacade, - model.dataModel.btcWalletService); - } catch (DelayedPayoutTxValidation.DonationAddressException | - DelayedPayoutTxValidation.InvalidTxException | - DelayedPayoutTxValidation.AmountMismatchException | - DelayedPayoutTxValidation.InvalidLockTimeException | - DelayedPayoutTxValidation.MissingDelayedPayoutTxException e) { - if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) { - new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show(); - } - } + protected void onPendingTradesInitialized() { + super.onPendingTradesInitialized(); + validatePayoutTx(); } @@ -86,6 +72,28 @@ public class BuyerStep1View extends TradeStepView { protected String getPeriodOverWarnText() { return Res.get("portfolio.pending.step1.openForDispute"); } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void validatePayoutTx() { + try { + TradeDataValidation.validatePayoutTx(trade, + trade.getDelayedPayoutTx(), + model.dataModel.daoFacade, + model.dataModel.btcWalletService); + } catch (TradeDataValidation.MissingTxException ignore) { + // We don't react on those errors as a failed trade might get listed initially but getting removed from the + // trade manager after initPendingTrades which happens after activate might be called. + } catch (TradeDataValidation.ValidationException e) { + if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) { + new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show(); + } + } + } + } 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 70d2718f08..150c38bca2 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 @@ -70,8 +70,8 @@ import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.USPostalMoneyOrderAccountPayload; import bisq.core.payment.payload.WesternUnionAccountPayload; -import bisq.core.trade.DelayedPayoutTxValidation; import bisq.core.trade.Trade; +import bisq.core.trade.TradeDataValidation; import bisq.core.user.DontShowAgainLookup; import bisq.common.Timer; @@ -117,21 +117,6 @@ public class BuyerStep2View extends TradeStepView { public void activate() { super.activate(); - try { - DelayedPayoutTxValidation.validatePayoutTx(trade, - trade.getDelayedPayoutTx(), - model.dataModel.daoFacade, - model.dataModel.btcWalletService); - } catch (DelayedPayoutTxValidation.DonationAddressException | - DelayedPayoutTxValidation.InvalidTxException | - DelayedPayoutTxValidation.AmountMismatchException | - DelayedPayoutTxValidation.InvalidLockTimeException | - DelayedPayoutTxValidation.MissingDelayedPayoutTxException e) { - if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) { - new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show(); - } - } - if (timeoutTimer != null) timeoutTimer.stop(); @@ -209,6 +194,13 @@ public class BuyerStep2View extends TradeStepView { } } + @Override + protected void onPendingTradesInitialized() { + super.onPendingTradesInitialized(); + validatePayoutTx(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Content /////////////////////////////////////////////////////////////////////////////////////////// @@ -623,6 +615,22 @@ public class BuyerStep2View extends TradeStepView { } } + private void validatePayoutTx() { + try { + TradeDataValidation.validatePayoutTx(trade, + trade.getDelayedPayoutTx(), + model.dataModel.daoFacade, + model.dataModel.btcWalletService); + } catch (TradeDataValidation.MissingTxException ignore) { + // We don't react on those errors as a failed trade might get listed initially but getting removed from the + // trade manager after initPendingTrades which happens after activate might be called. + } catch (TradeDataValidation.ValidationException e) { + if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) { + new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show(); + } + } + } + @Override protected void updateConfirmButtonDisableState(boolean isDisabled) { confirmButton.setDisable(isDisabled); diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeSummaryVerification.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeSummaryVerification.java new file mode 100644 index 0000000000..c806da3cd9 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeSummaryVerification.java @@ -0,0 +1,102 @@ +/* + * 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; + +import bisq.core.locale.Res; +import bisq.core.support.dispute.DisputeList; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.agent.DisputeAgent; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.crypto.CryptoException; +import bisq.common.crypto.Hash; +import bisq.common.crypto.Sig; +import bisq.common.util.Utilities; + +import java.security.KeyPair; +import java.security.PublicKey; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class DisputeSummaryVerification { + // Must not change as it is used for splitting the text for verifying the signature of the summary message + private static final String SEPARATOR1 = "\n-----BEGIN SIGNATURE-----\n"; + private static final String SEPARATOR2 = "\n-----END SIGNATURE-----\n"; + + public static String signAndApply(DisputeManager> disputeManager, + DisputeResult disputeResult, + String textToSign) { + + byte[] hash = Hash.getSha256Hash(textToSign); + KeyPair signatureKeyPair = disputeManager.getSignatureKeyPair(); + String sigAsHex; + try { + byte[] signature = Sig.sign(signatureKeyPair.getPrivate(), hash); + sigAsHex = Utilities.encodeToHex(signature); + disputeResult.setArbitratorSignature(signature); + } catch (CryptoException e) { + sigAsHex = "Signing failed"; + } + + return Res.get("disputeSummaryWindow.close.msgWithSig", + textToSign, + SEPARATOR1, + sigAsHex, + SEPARATOR2); + } + + public static String verifySignature(String input, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager) { + try { + String[] parts = input.split(SEPARATOR1); + String textToSign = parts[0]; + String fullAddress = textToSign.split("\n")[1].split(": ")[1]; + NodeAddress nodeAddress = new NodeAddress(fullAddress); + DisputeAgent disputeAgent = mediatorManager.getDisputeAgentByNodeAddress(nodeAddress).orElse(null); + if (disputeAgent == null) { + disputeAgent = refundAgentManager.getDisputeAgentByNodeAddress(nodeAddress).orElse(null); + } + checkNotNull(disputeAgent); + PublicKey pubKey = disputeAgent.getPubKeyRing().getSignaturePubKey(); + + String sigString = parts[1].split(SEPARATOR2)[0]; + byte[] sig = Utilities.decodeFromHex(sigString); + byte[] hash = Hash.getSha256Hash(textToSign); + try { + boolean result = Sig.verify(pubKey, hash, sig); + if (result) { + return Res.get("support.sigCheck.popup.success"); + } else { + return Res.get("support.sigCheck.popup.failed"); + } + } catch (CryptoException e) { + return Res.get("support.sigCheck.popup.failed"); + } + } catch (Throwable e) { + return Res.get("support.sigCheck.popup.invalidFormat"); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java index fcba3c3b65..e23df1f2eb 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeView.java @@ -28,12 +28,14 @@ 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.overlays.windows.VerifyDisputeResultSignatureWindow; import bisq.desktop.main.shared.ChatView; import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.GUIUtil; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; +import bisq.core.dao.DaoFacade; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.support.SupportType; @@ -42,6 +44,8 @@ import bisq.core.support.dispute.DisputeList; import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.DisputeSession; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.support.messages.ChatMessage; import bisq.core.trade.Contract; import bisq.core.trade.Trade; @@ -58,8 +62,6 @@ import bisq.common.util.Utilities; import org.bitcoinj.core.Coin; -import com.google.common.collect.Lists; - import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import javafx.scene.control.Button; @@ -89,9 +91,8 @@ import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import javafx.util.Callback; +import javafx.util.Duration; -import java.text.DateFormat; -import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -102,6 +103,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import lombok.Getter; @@ -110,6 +112,32 @@ import javax.annotation.Nullable; import static bisq.desktop.util.FormBuilder.getIconForLabel; public abstract class DisputeView extends ActivatableView { + public enum FilterResult { + NO_MATCH("No Match"), + NO_FILTER("No filter text"), + OPEN_DISPUTES("Open disputes"), + TRADE_ID("Trade ID"), + OPENING_DATE("Opening date"), + BUYER_NODE_ADDRESS("Buyer node address"), + SELLER_NODE_ADDRESS("Seller node address"), + BUYER_ACCOUNT_DETAILS("Buyer account details"), + SELLER_ACCOUNT_DETAILS("Seller account details"), + DEPOSIT_TX("Deposit tx ID"), + PAYOUT_TX("Payout tx ID"), + DEL_PAYOUT_TX("Delayed payout tx ID"), + RESULT_MESSAGE("Result message"), + REASON("Reason"), + JSON("Contract as json"); + + // Used in tooltip at search string to show where the match was found + @Getter + private final String displayString; + + FilterResult(String displayString) { + + this.displayString = displayString; + } + } protected final DisputeManager> disputeManager; protected final KeyRing keyRing; @@ -121,6 +149,9 @@ public abstract class DisputeView extends ActivatableView { private final TradeDetailsWindow tradeDetailsWindow; private final AccountAgeWitnessService accountAgeWitnessService; + private final MediatorManager mediatorManager; + private final RefundAgentManager refundAgentManager; + protected final DaoFacade daoFacade; private final boolean useDevPrivilegeKeys; protected TableView tableView; @@ -136,7 +167,7 @@ public abstract class DisputeView extends ActivatableView { protected FilteredList filteredList; protected InputTextField filterTextField; private ChangeListener filterTextFieldListener; - protected AutoTooltipButton reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton; + protected AutoTooltipButton sigCheckButton, reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton; private Map> disputeChatMessagesListeners = new HashMap<>(); @Nullable private ListChangeListener disputesListener; // Only set in mediation cases @@ -157,6 +188,9 @@ public abstract class DisputeView extends ActivatableView { ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, + DaoFacade daoFacade, boolean useDevPrivilegeKeys) { this.disputeManager = disputeManager; this.keyRing = keyRing; @@ -167,6 +201,9 @@ public abstract class DisputeView extends ActivatableView { this.contractWindow = contractWindow; this.tradeDetailsWindow = tradeDetailsWindow; this.accountAgeWitnessService = accountAgeWitnessService; + this.mediatorManager = mediatorManager; + this.refundAgentManager = refundAgentManager; + this.daoFacade = daoFacade; this.useDevPrivilegeKeys = useDevPrivilegeKeys; } @@ -177,6 +214,10 @@ public abstract class DisputeView extends ActivatableView { HBox.setHgrow(label, Priority.NEVER); filterTextField = new InputTextField(); + Tooltip tooltip = new Tooltip(); + tooltip.setShowDelay(Duration.millis(100)); + tooltip.setShowDuration(Duration.seconds(10)); + filterTextField.setTooltip(tooltip); filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText()); HBox.setHgrow(filterTextField, Priority.NEVER); @@ -222,6 +263,12 @@ public abstract class DisputeView extends ActivatableView { showFullReport(); }); + sigCheckButton = new AutoTooltipButton(Res.get("support.sigCheck.button")); + HBox.setHgrow(sigCheckButton, Priority.NEVER); + sigCheckButton.setOnAction(e -> { + new VerifyDisputeResultSignatureWindow(mediatorManager, refundAgentManager).show(); + }); + Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); @@ -234,7 +281,8 @@ public abstract class DisputeView extends ActivatableView { reOpenButton, sendPrivateNotificationButton, reportButton, - fullReportButton); + fullReportButton, + sigCheckButton); VBox.setVgrow(filterBox, Priority.NEVER); tableView = new TableView<>(); @@ -255,7 +303,8 @@ public abstract class DisputeView extends ActivatableView { protected void activate() { filterTextField.textProperty().addListener(filterTextFieldListener); - filteredList = new FilteredList<>(disputeManager.getDisputesAsObservableList()); + ObservableList disputesAsObservableList = disputeManager.getDisputesAsObservableList(); + filteredList = new FilteredList<>(disputesAsObservableList); applyFilteredListPredicate(filterTextField.getText()); sortedList = new SortedList<>(filteredList); @@ -276,54 +325,6 @@ public abstract class DisputeView extends ActivatableView { chatView.scrollToBottom(); } - - // If doPrint=true we print out a html page which opens tabs with all deposit txs - // (firefox needs about:config change to allow > 20 tabs) - // Useful to check if there any funds in not finished trades (no payout tx done). - // Last check 10.02.2017 found 8 trades and we contacted all traders as far as possible (email if available - // otherwise in-app private notification) - boolean doPrint = false; - //noinspection ConstantConditions - if (doPrint) { - try { - DateFormat formatter = new SimpleDateFormat("dd/MM/yy"); - //noinspection UnusedAssignment - Date startDate = formatter.parse("10/02/17"); - startDate = new Date(0); // print all from start - - HashMap map = new HashMap<>(); - disputeManager.getDisputesAsObservableList().forEach(dispute -> map.put(dispute.getDepositTxId(), dispute)); - - final Date finalStartDate = startDate; - List disputes = new ArrayList<>(map.values()); - disputes.sort(Comparator.comparing(Dispute::getOpeningDate)); - List> subLists = Lists.partition(disputes, 1000); - StringBuilder sb = new StringBuilder(); - // We don't translate that as it is not intended for the public - subLists.forEach(list -> { - StringBuilder sb1 = new StringBuilder("\n\n"); - list.forEach(dispute -> { - if (dispute.getOpeningDate().after(finalStartDate)) { - String txId = dispute.getDepositTxId(); - sb1.append("window.open(\"https://blockchain.info/tx/").append(txId).append("\", '_blank');\n"); - - sb2.append("Dispute ID: ").append(dispute.getId()). - append(" Tx ID: "). - append(""). - append(txId).append(" "). - append("Opening date: ").append(formatter.format(dispute.getOpeningDate())).append("
\n"); - } - }); - sb2.append(""); - String res = sb1.toString() + sb2.toString(); - - sb.append(res).append("\n\n\n"); - }); - log.info(sb.toString()); - } catch (ParseException ignore) { - } - } GUIUtil.requestFocus(filterTextField); } @@ -385,10 +386,89 @@ public abstract class DisputeView extends ActivatableView { 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.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())); + AtomicReference filterResult = new AtomicReference<>(FilterResult.NO_FILTER); + filteredList.setPredicate(dispute -> { + filterResult.set(getFilterResult(dispute, filterString)); + return filterResult.get() != FilterResult.NO_MATCH; + }); + + if (filterResult.get() == FilterResult.NO_MATCH) { + filterTextField.getTooltip().setText("No matches found"); + } else if (filterResult.get() == FilterResult.NO_FILTER) { + filterTextField.getTooltip().setText("No filter applied"); + } else if (filterResult.get() == FilterResult.OPEN_DISPUTES) { + filterTextField.getTooltip().setText("Show all open disputes"); + } else { + filterTextField.getTooltip().setText("Data matching filter string: " + filterResult.get().getDisplayString()); + } } + protected FilterResult getFilterResult(Dispute dispute, String filterTerm) { + String filter = filterTerm.toLowerCase(); + if (filter.isEmpty()) { + return FilterResult.NO_FILTER; + } + + // For open filter we do not want to continue further as json data would cause a match + if (filter.equalsIgnoreCase("open")) { + return !dispute.isClosed() ? FilterResult.OPEN_DISPUTES : FilterResult.NO_MATCH; + } + + if (dispute.getTradeId().toLowerCase().contains(filter)) { + return FilterResult.TRADE_ID; + } + + if (DisplayUtils.formatDate(dispute.getOpeningDate()).toLowerCase().contains(filter)) { + return FilterResult.OPENING_DATE; + } + + if (dispute.getContract().getBuyerNodeAddress().getFullAddress().contains(filter)) { + return FilterResult.BUYER_NODE_ADDRESS; + } + + if (dispute.getContract().getSellerNodeAddress().getFullAddress().contains(filter)) { + return FilterResult.SELLER_NODE_ADDRESS; + } + + if (dispute.getContract().getBuyerPaymentAccountPayload().getPaymentDetails().toLowerCase().contains(filter)) { + return FilterResult.BUYER_ACCOUNT_DETAILS; + } + + if (dispute.getContract().getSellerPaymentAccountPayload().getPaymentDetails().toLowerCase().contains(filter)) { + return FilterResult.SELLER_ACCOUNT_DETAILS; + } + + if (dispute.getDepositTxId() != null && dispute.getDepositTxId().contains(filter)) { + return FilterResult.DEPOSIT_TX; + } + if (dispute.getPayoutTxId() != null && dispute.getPayoutTxId().contains(filter)) { + return FilterResult.PAYOUT_TX; + } + + if (dispute.getDelayedPayoutTxId() != null && dispute.getDelayedPayoutTxId().contains(filter)) { + return FilterResult.DEL_PAYOUT_TX; + } + + DisputeResult disputeResult = dispute.getDisputeResultProperty().get(); + if (disputeResult != null) { + ChatMessage chatMessage = disputeResult.getChatMessage(); + if (chatMessage != null && chatMessage.getMessage().toLowerCase().contains(filter)) { + return FilterResult.RESULT_MESSAGE; + } + + if (disputeResult.getReason().name().toLowerCase().contains(filter)) { + return FilterResult.REASON; + } + } + + if (dispute.getContractAsJson().toLowerCase().contains(filter)) { + return FilterResult.JSON; + } + + return FilterResult.NO_MATCH; + } + + protected void reOpenDisputeFromButton() { reOpenDispute(); } @@ -412,16 +492,6 @@ public abstract class DisputeView extends ActivatableView { } } - protected boolean anyMatchOfFilterString(Dispute dispute, String filterString) { - boolean matchesTradeId = dispute.getTradeId().contains(filterString); - boolean matchesDate = DisplayUtils.formatDate(dispute.getOpeningDate()).contains(filterString); - boolean isBuyerOnion = dispute.getContract().getBuyerNodeAddress().getFullAddress().contains(filterString); - boolean isSellerOnion = dispute.getContract().getSellerNodeAddress().getFullAddress().contains(filterString); - boolean matchesBuyersPaymentAccountData = dispute.getContract().getBuyerPaymentAccountPayload().getPaymentDetails().contains(filterString); - boolean matchesSellersPaymentAccountData = dispute.getContract().getSellerPaymentAccountPayload().getPaymentDetails().contains(filterString); - return matchesTradeId || matchesDate || isBuyerOnion || isSellerOnion || - matchesBuyersPaymentAccountData || matchesSellersPaymentAccountData; - } /////////////////////////////////////////////////////////////////////////////////////////// // UI actions @@ -547,6 +617,36 @@ public abstract class DisputeView extends ActivatableView { map.forEach((key, value) -> allDisputes.add(value)); allDisputes.sort(Comparator.comparing(o -> !o.isEmpty() ? o.get(0).getOpeningDate() : new Date(0))); StringBuilder stringBuilder = new StringBuilder(); + StringBuilder csvStringBuilder = new StringBuilder(); + csvStringBuilder.append("Dispute nr").append(";") + .append("Closed during cycle").append(";") + .append("Status").append(";") + .append("Trade date").append(";") + .append("Trade ID").append(";") + .append("Offer version").append(";") + .append("Opening date").append(";") + .append("Close date").append(";") + .append("Duration").append(";") + .append("Currency").append(";") + .append("Trade amount").append(";") + .append("Payment method").append(";") + .append("Buyer account details").append(";") + .append("Seller account details").append(";") + .append("Buyer address").append(";") + .append("Seller address").append(";") + .append("Buyer security deposit").append(";") + .append("Seller security deposit").append(";") + .append("Dispute opened by").append(";") + .append("Payout to buyer").append(";") + .append("Payout to seller").append(";") + .append("Winner").append(";") + .append("Reason").append(";") + .append("Summary notes").append(";") + .append("Summary notes (other trader)"); + + Map blockStartDateByCycleIndex = daoFacade.getBlockStartDateByCycleIndex(); + + SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy MM dd HH:mm:ss"); AtomicInteger disputeIndex = new AtomicInteger(); allDisputes.forEach(disputesPerTrade -> { if (disputesPerTrade.size() > 0) { @@ -561,38 +661,76 @@ public abstract class DisputeView extends ActivatableView { disputeResult.getWinner() == DisputeResult.Winner.BUYER ? "Buyer" : "Seller"; String buyerPayoutAmount = disputeResult != null ? disputeResult.getBuyerPayoutAmount().toFriendlyString() : ""; String sellerPayoutAmount = disputeResult != null ? disputeResult.getSellerPayoutAmount().toFriendlyString() : ""; - stringBuilder.append("\n") - .append("Dispute nr. ") - .append(disputeIndex.incrementAndGet()) + + int index = disputeIndex.incrementAndGet(); + String tradeDateString = dateFormatter.format(firstDispute.getTradeDate()); + String openingDateString = dateFormatter.format(openingDate); + + // Index we display starts with 1 not with 0 + int cycleIndex = 0; + if (disputeResult != null) { + Date closeDate = disputeResult.getCloseDate(); + cycleIndex = blockStartDateByCycleIndex.entrySet().stream() + .filter(e -> e.getValue().after(closeDate)) + .findFirst() + .map(Map.Entry::getKey) + .orElse(0); + } + stringBuilder.append("\n").append("Dispute nr.: ").append(index).append("\n"); + + if (cycleIndex > 0) { + stringBuilder.append("Closed during cycle: ").append(cycleIndex).append("\n"); + } + stringBuilder.append("Trade date: ").append(tradeDateString) .append("\n") - .append("Opening date: ") - .append(DisplayUtils.formatDateTime(openingDate)) + .append("Opening date: ").append(openingDateString) .append("\n"); - String summaryNotes0 = ""; + String tradeId = firstDispute.getTradeId(); + csvStringBuilder.append("\n").append(index).append(";"); + if (cycleIndex > 0) { + csvStringBuilder.append(cycleIndex).append(";"); + } else { + csvStringBuilder.append(";"); + } + csvStringBuilder.append(firstDispute.isClosed() ? "Closed" : "Open").append(";") + .append(tradeDateString).append(";") + .append(firstDispute.getShortTradeId()).append(";") + .append(tradeId, tradeId.length() - 3, tradeId.length()).append(";") + .append(openingDateString).append(";"); + + String summaryNotes = ""; if (disputeResult != null) { Date closeDate = disputeResult.getCloseDate(); long duration = closeDate.getTime() - openingDate.getTime(); - stringBuilder.append("Close date: ") - .append(DisplayUtils.formatDateTime(closeDate)) - .append("\n") - .append("Dispute duration: ") - .append(FormattingUtils.formatDurationAsWords(duration)) - .append("\n"); + + String closeDateString = dateFormatter.format(closeDate); + String durationAsWords = FormattingUtils.formatDurationAsWords(duration); + stringBuilder.append("Close date: ").append(closeDateString).append("\n") + .append("Dispute duration: ").append(durationAsWords).append("\n"); + csvStringBuilder.append(closeDateString).append(";") + .append(durationAsWords).append(";"); + } else { + csvStringBuilder.append(";").append(";"); } + String paymentMethod = Res.get(contract.getPaymentMethodId()); + String currency = CurrencyUtil.getNameAndCode(contract.getOfferPayload().getCurrencyCode()); + String tradeAmount = contract.getTradeAmount().toFriendlyString(); + String buyerDeposit = Coin.valueOf(contract.getOfferPayload().getBuyerSecurityDeposit()).toFriendlyString(); + String sellerDeposit = Coin.valueOf(contract.getOfferPayload().getSellerSecurityDeposit()).toFriendlyString(); stringBuilder.append("Payment method: ") - .append(Res.get(contract.getPaymentMethodId())) + .append(paymentMethod) .append("\n") .append("Currency: ") - .append(CurrencyUtil.getNameAndCode(contract.getOfferPayload().getCurrencyCode())) + .append(currency) .append("\n") .append("Trade amount: ") - .append(contract.getTradeAmount().toFriendlyString()) + .append(tradeAmount) .append("\n") .append("Buyer/seller security deposit: ") - .append(Coin.valueOf(contract.getOfferPayload().getBuyerSecurityDeposit()).toFriendlyString()) + .append(buyerDeposit) .append("/") - .append(Coin.valueOf(contract.getOfferPayload().getSellerSecurityDeposit()).toFriendlyString()) + .append(sellerDeposit) .append("\n") .append("Dispute opened by: ") .append(opener) @@ -603,6 +741,28 @@ public abstract class DisputeView extends ActivatableView { .append(winner) .append(")\n"); + String buyerPaymentAccountPayload = Utilities.toTruncatedString( + contract.getBuyerPaymentAccountPayload().getPaymentDetails(). + replace("\n", " ").replace(";", "."), 100); + String sellerPaymentAccountPayload = Utilities.toTruncatedString( + contract.getSellerPaymentAccountPayload().getPaymentDetails() + .replace("\n", " ").replace(";", "."), 100); + String buyerNodeAddress = contract.getBuyerNodeAddress().getFullAddress(); + String sellerNodeAddress = contract.getSellerNodeAddress().getFullAddress(); + csvStringBuilder.append(currency).append(";") + .append(tradeAmount.replace(" BTC", "")).append(";") + .append(paymentMethod).append(";") + .append(buyerPaymentAccountPayload).append(";") + .append(sellerPaymentAccountPayload).append(";") + .append(buyerNodeAddress.replace(".onion:9999", "")).append(";") + .append(sellerNodeAddress.replace(".onion:9999", "")).append(";") + .append(buyerDeposit.replace(" BTC", "")).append(";") + .append(sellerDeposit.replace(" BTC", "")).append(";") + .append(opener).append(";") + .append(buyerPayoutAmount.replace(" BTC", "")).append(";") + .append(sellerPayoutAmount.replace(" BTC", "")).append(";") + .append(winner).append(";"); + if (disputeResult != null) { DisputeResult.Reason reason = disputeResult.getReason(); if (firstDispute.disputeResultProperty().get().getReason() != null) { @@ -611,10 +771,18 @@ public abstract class DisputeView extends ActivatableView { stringBuilder.append("Reason: ") .append(reason.name()) .append("\n"); + + csvStringBuilder.append(reason.name()).append(";"); + } else { + csvStringBuilder.append(";"); } - summaryNotes0 = disputeResult.getSummaryNotesProperty().get(); - stringBuilder.append("Summary notes: ").append(summaryNotes0).append("\n"); + summaryNotes = disputeResult.getSummaryNotesProperty().get(); + stringBuilder.append("Summary notes: ").append(summaryNotes).append("\n"); + + csvStringBuilder.append(summaryNotes).append(";"); + } else { + csvStringBuilder.append(";"); } // We might have a different summary notes at second trader. Only if it @@ -624,8 +792,12 @@ public abstract class DisputeView extends ActivatableView { DisputeResult disputeResult1 = dispute1.getDisputeResultProperty().get(); if (disputeResult1 != null) { String summaryNotes1 = disputeResult1.getSummaryNotesProperty().get(); - if (!summaryNotes1.equals(summaryNotes0)) { + if (!summaryNotes1.equals(summaryNotes)) { stringBuilder.append("Summary notes (different message to other trader was used): ").append(summaryNotes1).append("\n"); + + csvStringBuilder.append(summaryNotes1).append(";"); + } else { + csvStringBuilder.append(";"); } } } @@ -643,8 +815,9 @@ public abstract class DisputeView extends ActivatableView { .width(1200) .actionButtonText("Copy to clipboard") .onAction(() -> Utilities.copyToClipboard(message)) + .secondaryActionButtonText("Copy as csv data") + .onSecondaryAction(() -> Utilities.copyToClipboard(csvStringBuilder.toString())) .show(); - } private void showFullReport() { diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java index 13e5260867..41a1f2a3b6 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/DisputeAgentView.java @@ -27,12 +27,16 @@ import bisq.desktop.main.support.dispute.DisputeView; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; +import bisq.core.dao.DaoFacade; 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.support.dispute.agent.MultipleHolderNameDetection; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; +import bisq.core.trade.TradeDataValidation; import bisq.core.trade.TradeManager; import bisq.core.user.DontShowAgainLookup; import bisq.core.util.coin.CoinFormatter; @@ -54,13 +58,17 @@ import javafx.geometry.Insets; import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.collections.ListChangeListener; + import java.util.List; +import static bisq.core.trade.TradeDataValidation.ValidationException; import static bisq.desktop.util.FormBuilder.getIconForLabel; public abstract class DisputeAgentView extends DisputeView implements MultipleHolderNameDetection.Listener { private final MultipleHolderNameDetection multipleHolderNameDetection; + private ListChangeListener validationExceptionListener; public DisputeAgentView(DisputeManager> disputeManager, KeyRing keyRing, @@ -71,6 +79,9 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, + DaoFacade daoFacade, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, boolean useDevPrivilegeKeys) { super(disputeManager, keyRing, @@ -81,6 +92,9 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo contractWindow, tradeDetailsWindow, accountAgeWitnessService, + mediatorManager, + refundAgentManager, + daoFacade, useDevPrivilegeKeys); multipleHolderNameDetection = new MultipleHolderNameDetection(disputeManager); @@ -107,6 +121,32 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo fullReportButton.setManaged(true); multipleHolderNameDetection.detectMultipleHolderNames(); + + validationExceptionListener = c -> { + c.next(); + if (c.wasAdded()) { + showWarningForValidationExceptions(c.getAddedSubList()); + } + }; + } + + protected void showWarningForValidationExceptions(List exceptions) { + exceptions.stream() + .filter(ex -> ex.getDispute() != null) + .filter(ex -> !ex.getDispute().isClosed()) // we show warnings only for open cases + .forEach(ex -> { + Dispute dispute = ex.getDispute(); + if (ex instanceof TradeDataValidation.AddressException) { + new Popup().width(900).warning(Res.get("support.warning.disputesWithInvalidDonationAddress", + dispute.getDonationAddressOfDelayedPayoutTx(), + daoFacade.getAllDonationAddresses(), + dispute.getTradeId(), + "")) + .show(); + } else { + new Popup().width(900).warning(ex.getMessage()).show(); + } + }); } @Override @@ -117,6 +157,9 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo if (multipleHolderNameDetection.hasSuspiciousDisputesDetected()) { suspiciousDisputeDetected(); } + + disputeManager.getValidationExceptions().addListener(validationExceptionListener); + showWarningForValidationExceptions(disputeManager.getValidationExceptions()); } @Override @@ -124,6 +167,8 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo super.deactivate(); multipleHolderNameDetection.removeListener(this); + + disputeManager.getValidationExceptions().removeListener(validationExceptionListener); } @@ -142,17 +187,13 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo /////////////////////////////////////////////////////////////////////////////////////////// @Override - protected void applyFilteredListPredicate(String filterString) { - filteredList.setPredicate(dispute -> { - // If in arbitrator view we must only display disputes where we are selected as arbitrator (must not receive others anyway) - if (!dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) { - return false; - } - boolean isOpen = !dispute.isClosed() && filterString.toLowerCase().equals("open"); - return filterString.isEmpty() || - isOpen || - anyMatchOfFilterString(dispute, filterString); - }); + protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filterString) { + // If in arbitrator view we must only display disputes where we are selected as arbitrator (must not receive others anyway) + if (!dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) { + return FilterResult.NO_MATCH; + } + + return super.getFilterResult(dispute, filterString); } @Override 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 index dcb33c6112..7c8c40b141 100644 --- 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 @@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.agent.DisputeAgentView; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; +import bisq.core.dao.DaoFacade; 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.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeManager; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -53,6 +56,9 @@ public class ArbitratorView extends DisputeAgentView { ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, + DaoFacade daoFacade, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(arbitrationManager, keyRing, @@ -63,6 +69,9 @@ public class ArbitratorView extends DisputeAgentView { contractWindow, tradeDetailsWindow, accountAgeWitnessService, + daoFacade, + mediatorManager, + refundAgentManager, useDevPrivilegeKeys); } 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 index ba939c34d2..4dacc16ed2 100644 --- 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 @@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.agent.DisputeAgentView; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; +import bisq.core.dao.DaoFacade; 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.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeManager; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -53,6 +56,9 @@ public class MediatorView extends DisputeAgentView { ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, + DaoFacade daoFacade, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(mediationManager, keyRing, @@ -63,6 +69,9 @@ public class MediatorView extends DisputeAgentView { contractWindow, tradeDetailsWindow, accountAgeWitnessService, + daoFacade, + mediatorManager, + refundAgentManager, useDevPrivilegeKeys); } diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.java index 5aae5f53f8..797e4849bd 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/refund/RefundAgentView.java @@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.agent.DisputeAgentView; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; +import bisq.core.dao.DaoFacade; import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeSession; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.refund.RefundManager; import bisq.core.support.dispute.refund.RefundSession; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeManager; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -37,9 +40,8 @@ import bisq.core.util.coin.CoinFormatter; import bisq.common.config.Config; import bisq.common.crypto.KeyRing; -import javax.inject.Named; - import javax.inject.Inject; +import javax.inject.Named; @FxmlView public class RefundAgentView extends DisputeAgentView { @@ -54,6 +56,9 @@ public class RefundAgentView extends DisputeAgentView { ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, + DaoFacade daoFacade, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(refundManager, keyRing, @@ -64,6 +69,9 @@ public class RefundAgentView extends DisputeAgentView { contractWindow, tradeDetailsWindow, accountAgeWitnessService, + daoFacade, + mediatorManager, + refundAgentManager, useDevPrivilegeKeys); } 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 index f06875202e..33e31ce3bb 100644 --- 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 @@ -24,10 +24,13 @@ import bisq.desktop.main.support.dispute.DisputeView; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; +import bisq.core.dao.DaoFacade; 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.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeManager; import bisq.core.util.coin.CoinFormatter; @@ -43,9 +46,13 @@ public abstract class DisputeClientView extends DisputeView { ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, + DaoFacade daoFacade, boolean useDevPrivilegeKeys) { super(DisputeManager, keyRing, tradeManager, formatter, disputeSummaryWindow, privateNotificationManager, - contractWindow, tradeDetailsWindow, accountAgeWitnessService, useDevPrivilegeKeys); + contractWindow, tradeDetailsWindow, accountAgeWitnessService, + mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys); } @Override @@ -55,14 +62,12 @@ public abstract class DisputeClientView extends DisputeView { } @Override - protected void applyFilteredListPredicate(String filterString) { - filteredList.setPredicate(dispute -> { - // As we are in the client view we hide disputes where we are the agent - if (dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) { - return false; - } + protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filterString) { + // As we are in the client view we hide disputes where we are the agent + if (dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) { + return FilterResult.NO_MATCH; + } - return filterString.isEmpty() || anyMatchOfFilterString(dispute, filterString); - }); + return super.getFilterResult(dispute, filterString); } } 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 index 0ea4e14435..699463b84f 100644 --- 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 @@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.client.DisputeClientView; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; +import bisq.core.dao.DaoFacade; 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.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeManager; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -37,9 +40,8 @@ import bisq.core.util.coin.CoinFormatter; import bisq.common.config.Config; import bisq.common.crypto.KeyRing; -import javax.inject.Named; - import javax.inject.Inject; +import javax.inject.Named; @FxmlView public class ArbitrationClientView extends DisputeClientView { @@ -53,10 +55,13 @@ public class ArbitrationClientView extends DisputeClientView { ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, + DaoFacade daoFacade, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(arbitrationManager, keyRing, tradeManager, formatter, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, - useDevPrivilegeKeys); + mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys); } @Override 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 index eb60178e5f..02170e3835 100644 --- 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 @@ -26,12 +26,15 @@ import bisq.desktop.main.support.dispute.client.DisputeClientView; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; +import bisq.core.dao.DaoFacade; import bisq.core.locale.Res; 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.support.dispute.mediation.mediator.MediatorManager; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeManager; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -54,10 +57,13 @@ public class MediationClientView extends DisputeClientView { ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, + DaoFacade daoFacade, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(mediationManager, keyRing, tradeManager, formatter, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, - useDevPrivilegeKeys); + mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys); } @Override diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.java index 4f7b48d4fe..94cde1f8fe 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/client/refund/RefundClientView.java @@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.client.DisputeClientView; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.alert.PrivateNotificationManager; +import bisq.core.dao.DaoFacade; import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeSession; +import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.refund.RefundManager; import bisq.core.support.dispute.refund.RefundSession; +import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; import bisq.core.trade.TradeManager; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; @@ -37,9 +40,8 @@ import bisq.core.util.coin.CoinFormatter; import bisq.common.config.Config; import bisq.common.crypto.KeyRing; -import javax.inject.Named; - import javax.inject.Inject; +import javax.inject.Named; @FxmlView public class RefundClientView extends DisputeClientView { @@ -53,10 +55,13 @@ public class RefundClientView extends DisputeClientView { ContractWindow contractWindow, TradeDetailsWindow tradeDetailsWindow, AccountAgeWitnessService accountAgeWitnessService, + MediatorManager mediatorManager, + RefundAgentManager refundAgentManager, + DaoFacade daoFacade, @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { super(refundManager, keyRing, tradeManager, formatter, disputeSummaryWindow, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, - useDevPrivilegeKeys); + mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys); } @Override diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 1cb5dff3a8..9a1b55250d 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -795,6 +795,7 @@ message Dispute { SupportType support_type = 24; string mediators_dispute_result = 25; string delayed_payout_tx_id = 26; + string donation_address_of_delayed_payout_tx = 27; } message Attachment {