diff --git a/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java index 4132b1934a8..3abfa8b2aea 100644 --- a/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/BankAccountPayload.java @@ -39,7 +39,7 @@ @Getter @ToString @Slf4j -public abstract class BankAccountPayload extends CountryBasedPaymentAccountPayload { +public abstract class BankAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { protected String holderName = ""; @Nullable protected String bankName; diff --git a/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java index 10c39c0023a..afa97764a9d 100644 --- a/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/CashDepositAccountPayload.java @@ -42,7 +42,7 @@ @Setter @Getter @Slf4j -public class CashDepositAccountPayload extends CountryBasedPaymentAccountPayload { +public class CashDepositAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { private String holderName = ""; @Nullable private String holderEmail; diff --git a/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java index b8d93695e74..a320c36679e 100644 --- a/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/ChaseQuickPayAccountPayload.java @@ -37,7 +37,7 @@ @Setter @Getter @Slf4j -public final class ChaseQuickPayAccountPayload extends PaymentAccountPayload { +public final class ChaseQuickPayAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String email = ""; private String holderName = ""; diff --git a/core/src/main/java/bisq/core/payment/payload/ClearXchangeAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/ClearXchangeAccountPayload.java index 139da088893..f437d5d6720 100644 --- a/core/src/main/java/bisq/core/payment/payload/ClearXchangeAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/ClearXchangeAccountPayload.java @@ -37,7 +37,7 @@ @Setter @Getter @Slf4j -public final class ClearXchangeAccountPayload extends PaymentAccountPayload { +public final class ClearXchangeAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String emailOrMobileNr = ""; private String holderName = ""; diff --git a/core/src/main/java/bisq/core/payment/payload/InteracETransferAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/InteracETransferAccountPayload.java index f404766ad29..88b2042af87 100644 --- a/core/src/main/java/bisq/core/payment/payload/InteracETransferAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/InteracETransferAccountPayload.java @@ -39,7 +39,7 @@ @Setter @Getter @Slf4j -public final class InteracETransferAccountPayload extends PaymentAccountPayload { +public final class InteracETransferAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String email = ""; private String holderName = ""; private String question = ""; diff --git a/core/src/main/java/bisq/core/payment/payload/JapanBankAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/JapanBankAccountPayload.java index 4cfd4260e6f..807915b2bd8 100644 --- a/core/src/main/java/bisq/core/payment/payload/JapanBankAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/JapanBankAccountPayload.java @@ -37,7 +37,7 @@ @Setter @Getter @Slf4j -public final class JapanBankAccountPayload extends PaymentAccountPayload { +public final class JapanBankAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { // bank private String bankName = ""; private String bankCode = ""; @@ -137,4 +137,9 @@ public byte[] getAgeWitnessInputData() { String all = this.bankName + this.bankBranchName + this.bankAccountType + this.bankAccountNumber + this.bankAccountName; return super.getAgeWitnessInputData(all.getBytes(StandardCharsets.UTF_8)); } + + @Override + public String getHolderName() { + return bankAccountName; + } } diff --git a/core/src/main/java/bisq/core/payment/payload/MoneyGramAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/MoneyGramAccountPayload.java index e5f4ffc74d7..03734c68f68 100644 --- a/core/src/main/java/bisq/core/payment/payload/MoneyGramAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/MoneyGramAccountPayload.java @@ -39,7 +39,7 @@ @Setter @Getter @Slf4j -public class MoneyGramAccountPayload extends PaymentAccountPayload { +public class MoneyGramAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String holderName; private String countryCode = ""; private String state = ""; // is optional. we don't use @Nullable because it would makes UI code more complex. diff --git a/core/src/main/java/bisq/core/payment/payload/PayloadWithHolderName.java b/core/src/main/java/bisq/core/payment/payload/PayloadWithHolderName.java new file mode 100644 index 00000000000..25efe937f4a --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/PayloadWithHolderName.java @@ -0,0 +1,22 @@ +/* + * 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.payment.payload; + +public interface PayloadWithHolderName { + String getHolderName(); +} diff --git a/core/src/main/java/bisq/core/payment/payload/PopmoneyAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/PopmoneyAccountPayload.java index be018ffb558..3a451bda61c 100644 --- a/core/src/main/java/bisq/core/payment/payload/PopmoneyAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/PopmoneyAccountPayload.java @@ -37,7 +37,7 @@ @Setter @Getter @Slf4j -public final class PopmoneyAccountPayload extends PaymentAccountPayload { +public final class PopmoneyAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String accountId = ""; private String holderName = ""; diff --git a/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java index c10dacb8437..59425e1bcfe 100644 --- a/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/SepaAccountPayload.java @@ -43,7 +43,7 @@ @ToString @Getter @Slf4j -public final class SepaAccountPayload extends CountryBasedPaymentAccountPayload { +public final class SepaAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { @Setter private String holderName = ""; @Setter @@ -158,6 +158,7 @@ public byte[] getAgeWitnessInputData() { // slight changes in holder name (e.g. add or remove middle name) return super.getAgeWitnessInputData(ArrayUtils.addAll(iban.getBytes(StandardCharsets.UTF_8), bic.getBytes(StandardCharsets.UTF_8))); } + @Override public String getOwnerId() { return holderName; diff --git a/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java index cb8b69cd6f8..54cffcd78d2 100644 --- a/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/SepaInstantAccountPayload.java @@ -43,7 +43,7 @@ @ToString @Getter @Slf4j -public final class SepaInstantAccountPayload extends CountryBasedPaymentAccountPayload { +public final class SepaInstantAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { @Setter private String holderName = ""; @Setter diff --git a/core/src/main/java/bisq/core/payment/payload/SwishAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/SwishAccountPayload.java index 6c236ba1304..f14eafb92e4 100644 --- a/core/src/main/java/bisq/core/payment/payload/SwishAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/SwishAccountPayload.java @@ -37,7 +37,7 @@ @Setter @Getter @Slf4j -public final class SwishAccountPayload extends PaymentAccountPayload { +public final class SwishAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String mobileNr = ""; private String holderName = ""; diff --git a/core/src/main/java/bisq/core/payment/payload/USPostalMoneyOrderAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/USPostalMoneyOrderAccountPayload.java index e705e032dc8..96a8dec5203 100644 --- a/core/src/main/java/bisq/core/payment/payload/USPostalMoneyOrderAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/USPostalMoneyOrderAccountPayload.java @@ -39,7 +39,7 @@ @Setter @Getter @Slf4j -public final class USPostalMoneyOrderAccountPayload extends PaymentAccountPayload { +public final class USPostalMoneyOrderAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName { private String postalAddress = ""; private String holderName = ""; diff --git a/core/src/main/java/bisq/core/payment/payload/WesternUnionAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/WesternUnionAccountPayload.java index f99b80e81f0..45f33186cc3 100644 --- a/core/src/main/java/bisq/core/payment/payload/WesternUnionAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/WesternUnionAccountPayload.java @@ -39,7 +39,7 @@ @Setter @Getter @Slf4j -public class WesternUnionAccountPayload extends CountryBasedPaymentAccountPayload { +public class WesternUnionAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName { private String holderName; private String city; private String state = ""; // is optional. we don't use @Nullable because it would makes UI code more complex. diff --git a/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java b/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java new file mode 100644 index 00000000000..48025ae2669 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java @@ -0,0 +1,270 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.support.dispute.agent; + +import bisq.core.locale.Res; +import bisq.core.payment.payload.PayloadWithHolderName; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeList; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.trade.Contract; +import bisq.core.user.DontShowAgainLookup; + +import bisq.common.crypto.Hash; +import bisq.common.crypto.PubKeyRing; +import bisq.common.util.Tuple2; +import bisq.common.util.Utilities; + +import javafx.collections.ListChangeListener; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * Detects traders who had disputes where they used different account holder names. Only payment methods where a + * real name is required are used for the check. + * Strings are not translated here as it is only visible to dispute agents + */ +@Slf4j +public class MultipleHolderNameDetection { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + public interface Listener { + void onSuspiciousDisputeDetected(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static + /////////////////////////////////////////////////////////////////////////////////////////// + + private static final String ACK_KEY = "Ack-"; + + private static String getSigPuKeyHashAsHex(PubKeyRing pubKeyRing) { + return Utilities.encodeToHex(Hash.getRipemd160hash(pubKeyRing.getSignaturePubKeyBytes())); + } + + private static String getSigPubKeyHashAsHex(Dispute dispute) { + return getSigPuKeyHashAsHex(dispute.getTraderPubKeyRing()); + } + + private static boolean isBuyer(Dispute dispute) { + String traderSigPubKeyHashAsHex = getSigPubKeyHashAsHex(dispute); + String buyerSigPubKeyHashAsHex = getSigPuKeyHashAsHex(dispute.getContract().getBuyerPubKeyRing()); + return buyerSigPubKeyHashAsHex.equals(traderSigPubKeyHashAsHex); + } + + private static PayloadWithHolderName getPayloadWithHolderName(Dispute dispute) { + return (PayloadWithHolderName) getPaymentAccountPayload(dispute); + } + + public static PaymentAccountPayload getPaymentAccountPayload(Dispute dispute) { + return isBuyer(dispute) ? + dispute.getContract().getBuyerPaymentAccountPayload() : + dispute.getContract().getSellerPaymentAccountPayload(); + } + + public static String getAddress(Dispute dispute) { + return isBuyer(dispute) ? + dispute.getContract().getBuyerNodeAddress().getHostName() : + dispute.getContract().getSellerNodeAddress().getHostName(); + } + + public static String getAckKey(Dispute dispute) { + return ACK_KEY + getSigPubKeyHashAsHex(dispute).substring(0, 4) + "/" + dispute.getShortTradeId(); + } + + private static String getIsBuyerSubString(boolean isBuyer) { + return "'\n Role: " + (isBuyer ? "'Buyer'" : "'Seller'"); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Class fields + /////////////////////////////////////////////////////////////////////////////////////////// + + private final DisputeManager> disputeManager; + + // Key is hex of hash of sig pubKey which we consider a trader identity. We could use onion address as well but + // once we support multiple onion addresses that would not work anymore. + @Getter + private Map> suspiciousDisputesByTraderMap = new HashMap<>(); + private List listeners = new CopyOnWriteArrayList<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public MultipleHolderNameDetection(DisputeManager> disputeManager) { + this.disputeManager = disputeManager; + + disputeManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) { + detectMultipleHolderNames(); + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void detectMultipleHolderNames() { + String previous = suspiciousDisputesByTraderMap.toString(); + getAllDisputesByTraderMap().forEach((key, value) -> { + Set userNames = value.stream() + .map(dispute -> getPayloadWithHolderName(dispute).getHolderName()) + .collect(Collectors.toSet()); + if (userNames.size() > 1) { + // As we compare previous results we need to make sorting deterministic + value.sort(Comparator.comparing(Dispute::getId)); + suspiciousDisputesByTraderMap.put(key, value); + } + }); + String updated = suspiciousDisputesByTraderMap.toString(); + if (!previous.equals(updated)) { + listeners.forEach(Listener::onSuspiciousDisputeDetected); + } + } + + public boolean hasSuspiciousDisputesDetected() { + return !suspiciousDisputesByTraderMap.isEmpty(); + } + + // Returns all disputes of a trader who used multiple names + public List getDisputesForTrader(Dispute dispute) { + String traderPubKeyHash = getSigPubKeyHashAsHex(dispute); + if (suspiciousDisputesByTraderMap.containsKey(traderPubKeyHash)) { + return suspiciousDisputesByTraderMap.get(traderPubKeyHash); + } + return new ArrayList<>(); + } + + // Get a report of traders who used multiple names with all their disputes listed + public String getReportForAllDisputes() { + return getReport(suspiciousDisputesByTraderMap.values()); + } + + // Get a report for a trader who used multiple names with all their disputes listed + public String getReportForDisputeOfTrader(List disputes) { + Collection> values = new ArrayList<>(); + values.add(disputes); + return getReport(values); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private Map> getAllDisputesByTraderMap() { + Map> allDisputesByTraderMap = new HashMap<>(); + disputeManager.getDisputesAsObservableList() + .forEach(dispute -> { + Contract contract = dispute.getContract(); + PaymentAccountPayload paymentAccountPayload = isBuyer(dispute) ? + contract.getBuyerPaymentAccountPayload() : + contract.getSellerPaymentAccountPayload(); + if (paymentAccountPayload instanceof PayloadWithHolderName) { + String traderPubKeyHash = getSigPubKeyHashAsHex(dispute); + allDisputesByTraderMap.putIfAbsent(traderPubKeyHash, new ArrayList<>()); + List disputes = allDisputesByTraderMap.get(traderPubKeyHash); + disputes.add(dispute); + } + }); + return allDisputesByTraderMap; + } + + // Get a text report for a trader who used multiple names and list all the his disputes + private String getReport(Collection> collectionOfDisputesOfTrader) { + return collectionOfDisputesOfTrader.stream() + .map(disputes -> { + Set addresses = new HashSet<>(); + Set isBuyerHashSet = new HashSet<>(); + Set names = new HashSet<>(); + String disputesReport = disputes.stream() + .map(dispute -> { + addresses.add(getAddress(dispute)); + String ackKey = getAckKey(dispute); + String ackSubString = " "; + if (!DontShowAgainLookup.showAgain(ackKey)) { + ackSubString = "[ACK] "; + } + String holderName = getPayloadWithHolderName(dispute).getHolderName(); + names.add(holderName); + boolean isBuyer = isBuyer(dispute); + isBuyerHashSet.add(isBuyer); + String isBuyerSubString = getIsBuyerSubString(isBuyer); + DisputeResult disputeResult = dispute.disputeResultProperty().get(); + String summaryNotes = disputeResult != null ? disputeResult.getSummaryNotesProperty().get().trim() : "Not closed yet"; + return ackSubString + + "Trade ID: '" + dispute.getShortTradeId() + + "'\n Account holder name: '" + holderName + + "'\n Payment method: '" + Res.get(getPaymentAccountPayload(dispute).getPaymentMethodId()) + + isBuyerSubString + + "'\n Summary: '" + summaryNotes; + }) + .collect(Collectors.joining("\n")); + + String addressSubString = addresses.size() > 1 ? + "used multiple addresses " + addresses + " with" : + "with address " + new ArrayList<>(addresses).get(0) + " used"; + + String roleSubString = "Trader "; + if (isBuyerHashSet.size() == 1) { + boolean isBuyer = new ArrayList<>(isBuyerHashSet).get(0); + String isBuyerSubString = getIsBuyerSubString(isBuyer); + disputesReport = disputesReport.replace(isBuyerSubString, ""); + roleSubString = isBuyer ? "Buyer " : "Seller "; + } + + + String traderReport = roleSubString + addressSubString + " multiple names: " + names.toString() + "\n" + disputesReport; + return new Tuple2<>(roleSubString, traderReport); + }) + .sorted(Comparator.comparing(o -> o.first)) // Buyers first, then seller, then mixed (trader was in seller and buyer role) + .map(e -> e.second) + .collect(Collectors.joining("\n\n")); + } +} diff --git a/core/src/main/java/bisq/core/support/messages/ChatMessage.java b/core/src/main/java/bisq/core/support/messages/ChatMessage.java index a4e328ec390..b798a374639 100644 --- a/core/src/main/java/bisq/core/support/messages/ChatMessage.java +++ b/core/src/main/java/bisq/core/support/messages/ChatMessage.java @@ -51,7 +51,7 @@ import javax.annotation.Nullable; /* Message for direct communication between two nodes. Originally built for trader to - * arbitrator communication as no other direct communication was allowed. Aribtrator is + * arbitrator communication as no other direct communication was allowed. Arbitrator is * considered as the server and trader as the client in arbitration chats * * For trader to trader communication the maker is considered to be the server diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index 90e24fc0c47..114f272eda1 100644 --- a/desktop/src/main/java/bisq/desktop/bisq.css +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -816,6 +816,11 @@ tree-table-view:focused { -fx-padding: 27 2 0 2; } +.alert-icon { + -fx-fill: -bs-rd-error-red; + -fx-cursor: hand; +} + .close-icon { -fx-fill: -bs-text-color; } 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 6bf28f446cb..fcba3c3b652 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 @@ -60,18 +60,19 @@ import com.google.common.collect.Lists; -import javafx.scene.Scene; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; -import javafx.scene.input.KeyEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import javafx.scene.text.Text; import javafx.geometry.Insets; @@ -82,8 +83,6 @@ import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; -import javafx.event.EventHandler; - import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; @@ -108,6 +107,8 @@ import javax.annotation.Nullable; +import static bisq.desktop.util.FormBuilder.getIconForLabel; + public abstract class DisputeView extends ActivatableView { protected final DisputeManager> disputeManager; @@ -122,7 +123,7 @@ public abstract class DisputeView extends ActivatableView { private final AccountAgeWitnessService accountAgeWitnessService; private final boolean useDevPrivilegeKeys; - private TableView tableView; + protected TableView tableView; private SortedList sortedList; @Getter @@ -132,16 +133,15 @@ public abstract class DisputeView extends ActivatableView { private ChangeListener selectedDisputeClosedPropertyListener; private Subscription selectedDisputeSubscription; - private EventHandler keyEventEventHandler; - private Scene scene; protected FilteredList filteredList; protected InputTextField filterTextField; private ChangeListener filterTextFieldListener; - private HBox filterBox; protected AutoTooltipButton reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton; private Map> disputeChatMessagesListeners = new HashMap<>(); @Nullable private ListChangeListener disputesListener; // Only set in mediation cases + protected Label alertIconLabel; + protected TableColumn stateColumn; /////////////////////////////////////////////////////////////////////////////////////////// @@ -180,6 +180,14 @@ public void initialize() { filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText()); HBox.setHgrow(filterTextField, Priority.NEVER); + alertIconLabel = new Label(); + Text icon = getIconForLabel(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, "2em", alertIconLabel); + icon.getStyleClass().add("alert-icon"); + HBox.setMargin(alertIconLabel, new Insets(4, 0, 0, 10)); + alertIconLabel.setMouseTransparent(false); + alertIconLabel.setVisible(false); + alertIconLabel.setManaged(false); + reOpenButton = new AutoTooltipButton(Res.get("support.reOpenButton.label")); reOpenButton.setDisable(true); reOpenButton.setVisible(false); @@ -217,9 +225,16 @@ public void initialize() { Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); - filterBox = new HBox(); + HBox filterBox = new HBox(); filterBox.setSpacing(5); - filterBox.getChildren().addAll(label, filterTextField, spacer, reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton); + filterBox.getChildren().addAll(label, + filterTextField, + alertIconLabel, + spacer, + reOpenButton, + sendPrivateNotificationButton, + reportButton, + fullReportButton); VBox.setVgrow(filterBox, Priority.NEVER); tableView = new TableView<>(); @@ -232,8 +247,6 @@ public void initialize() { selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> chatView.setInputBoxVisible(!newValue); - keyEventEventHandler = this::handleKeyPressed; - chatView = new ChatView(disputeManager, formatter); chatView.initialize(); } @@ -263,9 +276,6 @@ else if (sortedList.size() > 0) chatView.scrollToBottom(); } - scene = root.getScene(); - if (scene != null) - scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); // 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) @@ -324,9 +334,6 @@ protected void deactivate() { selectedDisputeSubscription.unsubscribe(); removeListenersOnSelectDispute(); - if (scene != null) - scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); - if (chatView != null) chatView.deactivate(); } @@ -398,9 +405,6 @@ protected void onCloseDispute(Dispute dispute) { } } - protected void handleKeyPressed(KeyEvent event) { - } - protected void reOpenDispute() { if (selectedDispute != null) { selectedDispute.setIsClosed(false); @@ -712,7 +716,7 @@ private void showFullReport() { // Table /////////////////////////////////////////////////////////////////////////////////////////// - private void setupTable() { + protected void setupTable() { tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); Label placeholder = new AutoTooltipLabel(Res.get("support.noTickets")); placeholder.setWrapText(true); @@ -743,7 +747,7 @@ private void setupTable() { TableColumn roleColumn = getRoleColumn(); tableView.getColumns().add(roleColumn); - TableColumn stateColumn = getStateColumn(); + stateColumn = getStateColumn(); tableView.getColumns().add(stateColumn); tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId)); @@ -1099,7 +1103,6 @@ public void updateItem(final Dispute item, boolean empty) { }); return column; } - } 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 582c461b087..13e52608678 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 @@ -18,6 +18,8 @@ package bisq.desktop.main.support.dispute.agent; import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.ContractWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; @@ -30,14 +32,35 @@ 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.trade.TradeManager; +import bisq.core.user.DontShowAgainLookup; import bisq.core.util.coin.CoinFormatter; import bisq.common.crypto.KeyRing; +import bisq.common.util.Utilities; + +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.scene.text.Text; + +import javafx.geometry.Insets; + +import javafx.beans.property.ReadOnlyObjectWrapper; + +import java.util.List; + +import static bisq.desktop.util.FormBuilder.getIconForLabel; + +public abstract class DisputeAgentView extends DisputeView implements MultipleHolderNameDetection.Listener { -public abstract class DisputeAgentView extends DisputeView { + private final MultipleHolderNameDetection multipleHolderNameDetection; public DisputeAgentView(DisputeManager> disputeManager, KeyRing keyRing, @@ -59,8 +82,15 @@ public DisputeAgentView(DisputeManager { @@ -101,6 +165,131 @@ protected void handleOnSelectDispute(Dispute dispute) { DisputeSession chatSession = getConcreteDisputeChatSession(dispute); chatView.display(chatSession, closeDisputeButton, root.widthProperty()); } + + @Override + protected void setupTable() { + super.setupTable(); + + stateColumn.getStyleClass().remove("last-column"); + tableView.getColumns().add(getAlertColumn()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void suspiciousDisputeDetected() { + alertIconLabel.setVisible(true); + alertIconLabel.setManaged(true); + alertIconLabel.setTooltip(new Tooltip("You have suspicious disputes where the same trader used different " + + "account holder names.\nClick for more information.")); + // Text below is for arbitrators only so no need to translate it + alertIconLabel.setOnMouseClicked(e -> { + String reportForAllDisputes = multipleHolderNameDetection.getReportForAllDisputes(); + new Popup() + .width(1100) + .warning(getReportMessage(reportForAllDisputes, "traders")) + .actionButtonText(Res.get("shared.copyToClipboard")) + .onAction(() -> Utilities.copyToClipboard(reportForAllDisputes)) + .show(); + }); + } + + + private TableColumn getAlertColumn() { + TableColumn column = new AutoTooltipTableColumn<>("Alert") { + { + setMinWidth(50); + } + }; + column.getStyleClass().add("last-column"); + column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue())); + column.setCellFactory( + c -> new TableCell<>() { + Label alertIconLabel; + + @Override + public void updateItem(Dispute dispute, boolean empty) { + if (dispute != null && !empty) { + if (!showAlertAtDispute(dispute)) { + setGraphic(null); + if (alertIconLabel != null) { + alertIconLabel.setOnMouseClicked(null); + } + return; + } + + if (alertIconLabel != null) { + alertIconLabel.setOnMouseClicked(null); + } + + alertIconLabel = new Label(); + Text icon = getIconForLabel(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, "1.5em", alertIconLabel); + icon.getStyleClass().add("alert-icon"); + HBox.setMargin(alertIconLabel, new Insets(4, 0, 0, 10)); + alertIconLabel.setMouseTransparent(false); + setGraphic(alertIconLabel); + + alertIconLabel.setOnMouseClicked(e -> { + List realNameAccountInfoList = multipleHolderNameDetection.getDisputesForTrader(dispute); + String reportForDisputeOfTrader = multipleHolderNameDetection.getReportForDisputeOfTrader(realNameAccountInfoList); + String key = MultipleHolderNameDetection.getAckKey(dispute); + new Popup() + .width(1100) + .warning(getReportMessage(reportForDisputeOfTrader, "this trader")) + .actionButtonText(Res.get("shared.copyToClipboard")) + .onAction(() -> { + Utilities.copyToClipboard(reportForDisputeOfTrader); + if (!DontShowAgainLookup.showAgain(key)) { + setGraphic(null); + } + }) + .dontShowAgainId(key) + .dontShowAgainText("Is not suspicious") + .onClose(() -> { + if (!DontShowAgainLookup.showAgain(key)) { + setGraphic(null); + } + }) + .show(); + }); + } else { + setGraphic(null); + if (alertIconLabel != null) { + alertIconLabel.setOnMouseClicked(null); + } + } + } + }); + + column.setComparator((o1, o2) -> Boolean.compare(showAlertAtDispute(o1), showAlertAtDispute(o2))); + column.setSortable(true); + return column; + } + + private boolean showAlertAtDispute(Dispute dispute) { + return DontShowAgainLookup.showAgain(MultipleHolderNameDetection.getAckKey(dispute)) && + !multipleHolderNameDetection.getDisputesForTrader(dispute).isEmpty(); + } + + private String getReportMessage(String report, String subString) { + return "You have dispute cases where " + subString + " used different account holder names.\n\n" + + "This might be not critical in case of small variations of the same name " + + "(e.g. first name and last name are swapped), " + + "but if the name is completely different you should request information from the trader why they " + + "used a different name and request proof that the person with the real name is aware " + + "of the trade. " + + "It can be that the trader uses the account of their wife/husband, but it also could " + + "be a case of a stolen bank account or money laundering.\n\n" + + "Please check below the list of the names which have been detected. " + + "Search with the trade ID for the dispute case or check out the alert icon at each dispute in " + + "the list (you might need to remove the 'open' filter) and evaluate " + + "if it might be a fraudulent account (buyer role is more likely to be fraudulent). " + + "If you find suspicious disputes, please notify the developers and provide the contract json data " + + "to them so they can ban those traders.\n\n" + + Utilities.toTruncatedString(report, 700, false); + } }