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 extends DisputeList extends DisputeList>> 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 extends DisputeList extends DisputeList>> 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 extends DisputeList extends DisputeList>> 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 extends DisputeList extends DisputeList>> disputeManager,
KeyRing keyRing,
@@ -59,8 +82,15 @@ public DisputeAgentView(DisputeManager extends DisputeList extends DisputeLi
tradeDetailsWindow,
accountAgeWitnessService,
useDevPrivilegeKeys);
+
+ multipleHolderNameDetection = new MultipleHolderNameDetection(disputeManager);
}
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Life cycle
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
@Override
public void initialize() {
super.initialize();
@@ -75,8 +105,42 @@ public void initialize() {
fullReportButton.setVisible(true);
fullReportButton.setManaged(true);
+
+ multipleHolderNameDetection.detectMultipleHolderNames();
+ }
+
+ @Override
+ protected void activate() {
+ super.activate();
+
+ multipleHolderNameDetection.addListener(this);
+ if (multipleHolderNameDetection.hasSuspiciousDisputesDetected()) {
+ suspiciousDisputeDetected();
+ }
+ }
+
+ @Override
+ protected void deactivate() {
+ super.deactivate();
+
+ multipleHolderNameDetection.removeListener(this);
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // MultipleHolderNamesDetection.Listener
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public void onSuspiciousDisputeDetected() {
+ suspiciousDisputeDetected();
}
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // DisputeView
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
@Override
protected void applyFilteredListPredicate(String filterString) {
filteredList.setPredicate(dispute -> {
@@ -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);
+ }
}