Merge pull request #4484 from chimp1984/detect-accounts-with-diff-real-names

Scan disputes for accounts where same user used diff. real names.
This commit is contained in:
sqrrm 2020-09-06 23:17:32 +02:00 committed by GitHub
commit c188284d0b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 534 additions and 39 deletions

View file

@ -39,7 +39,7 @@ import javax.annotation.Nullable;
@Getter
@ToString
@Slf4j
public abstract class BankAccountPayload extends CountryBasedPaymentAccountPayload {
public abstract class BankAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName {
protected String holderName = "";
@Nullable
protected String bankName;

View file

@ -42,7 +42,7 @@ import javax.annotation.Nullable;
@Setter
@Getter
@Slf4j
public class CashDepositAccountPayload extends CountryBasedPaymentAccountPayload {
public class CashDepositAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName {
private String holderName = "";
@Nullable
private String holderEmail;

View file

@ -37,7 +37,7 @@ import lombok.extern.slf4j.Slf4j;
@Setter
@Getter
@Slf4j
public final class ChaseQuickPayAccountPayload extends PaymentAccountPayload {
public final class ChaseQuickPayAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName {
private String email = "";
private String holderName = "";

View file

@ -37,7 +37,7 @@ import lombok.extern.slf4j.Slf4j;
@Setter
@Getter
@Slf4j
public final class ClearXchangeAccountPayload extends PaymentAccountPayload {
public final class ClearXchangeAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName {
private String emailOrMobileNr = "";
private String holderName = "";

View file

@ -39,7 +39,7 @@ import lombok.extern.slf4j.Slf4j;
@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 = "";

View file

@ -37,7 +37,7 @@ import lombok.extern.slf4j.Slf4j;
@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 final class JapanBankAccountPayload extends PaymentAccountPayload {
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;
}
}

View file

@ -39,7 +39,7 @@ import lombok.extern.slf4j.Slf4j;
@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.

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.core.payment.payload;
public interface PayloadWithHolderName {
String getHolderName();
}

View file

@ -37,7 +37,7 @@ import lombok.extern.slf4j.Slf4j;
@Setter
@Getter
@Slf4j
public final class PopmoneyAccountPayload extends PaymentAccountPayload {
public final class PopmoneyAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName {
private String accountId = "";
private String holderName = "";

View file

@ -43,7 +43,7 @@ import lombok.extern.slf4j.Slf4j;
@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 final class SepaAccountPayload extends CountryBasedPaymentAccountPayload
// 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;

View file

@ -43,7 +43,7 @@ import lombok.extern.slf4j.Slf4j;
@ToString
@Getter
@Slf4j
public final class SepaInstantAccountPayload extends CountryBasedPaymentAccountPayload {
public final class SepaInstantAccountPayload extends CountryBasedPaymentAccountPayload implements PayloadWithHolderName {
@Setter
private String holderName = "";
@Setter

View file

@ -37,7 +37,7 @@ import lombok.extern.slf4j.Slf4j;
@Setter
@Getter
@Slf4j
public final class SwishAccountPayload extends PaymentAccountPayload {
public final class SwishAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName {
private String mobileNr = "";
private String holderName = "";

View file

@ -39,7 +39,7 @@ import lombok.extern.slf4j.Slf4j;
@Setter
@Getter
@Slf4j
public final class USPostalMoneyOrderAccountPayload extends PaymentAccountPayload {
public final class USPostalMoneyOrderAccountPayload extends PaymentAccountPayload implements PayloadWithHolderName {
private String postalAddress = "";
private String holderName = "";

View file

@ -39,7 +39,7 @@ import lombok.extern.slf4j.Slf4j;
@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.

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, List<Dispute>> suspiciousDisputesByTraderMap = new HashMap<>();
private List<Listener> listeners = new CopyOnWriteArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public MultipleHolderNameDetection(DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager) {
this.disputeManager = disputeManager;
disputeManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) c -> {
c.next();
if (c.wasAdded()) {
detectMultipleHolderNames();
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void detectMultipleHolderNames() {
String previous = suspiciousDisputesByTraderMap.toString();
getAllDisputesByTraderMap().forEach((key, value) -> {
Set<String> 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<Dispute> 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<Dispute> disputes) {
Collection<List<Dispute>> 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<String, List<Dispute>> getAllDisputesByTraderMap() {
Map<String, List<Dispute>> 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<Dispute> 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<List<Dispute>> collectionOfDisputesOfTrader) {
return collectionOfDisputesOfTrader.stream()
.map(disputes -> {
Set<String> addresses = new HashSet<>();
Set<Boolean> isBuyerHashSet = new HashSet<>();
Set<String> 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"));
}
}

View file

@ -51,7 +51,7 @@ import lombok.extern.slf4j.Slf4j;
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

View file

@ -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;
}

View file

@ -60,18 +60,19 @@ import org.bitcoinj.core.Coin;
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.ReadOnlyBooleanProperty;
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 lombok.Getter;
import javax.annotation.Nullable;
import static bisq.desktop.util.FormBuilder.getIconForLabel;
public abstract class DisputeView extends ActivatableView<VBox, Void> {
protected final DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager;
@ -122,7 +123,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
private final AccountAgeWitnessService accountAgeWitnessService;
private final boolean useDevPrivilegeKeys;
private TableView<Dispute> tableView;
protected TableView<Dispute> tableView;
private SortedList<Dispute> sortedList;
@Getter
@ -132,16 +133,15 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
private ChangeListener<Boolean> selectedDisputeClosedPropertyListener;
private Subscription selectedDisputeSubscription;
private EventHandler<KeyEvent> keyEventEventHandler;
private Scene scene;
protected FilteredList<Dispute> filteredList;
protected InputTextField filterTextField;
private ChangeListener<String> filterTextFieldListener;
private HBox filterBox;
protected AutoTooltipButton reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton;
private Map<String, ListChangeListener<ChatMessage>> disputeChatMessagesListeners = new HashMap<>();
@Nullable
private ListChangeListener<Dispute> disputesListener; // Only set in mediation cases
protected Label alertIconLabel;
protected TableColumn<Dispute, Dispute> stateColumn;
///////////////////////////////////////////////////////////////////////////////////////////
@ -180,6 +180,14 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
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 abstract class DisputeView extends ActivatableView<VBox, Void> {
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 abstract class DisputeView extends ActivatableView<VBox, Void> {
selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> chatView.setInputBoxVisible(!newValue);
keyEventEventHandler = this::handleKeyPressed;
chatView = new ChatView(disputeManager, formatter);
chatView.initialize();
}
@ -263,9 +276,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
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 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
selectedDisputeSubscription.unsubscribe();
removeListenersOnSelectDispute();
if (scene != null)
scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler);
if (chatView != null)
chatView.deactivate();
}
@ -398,9 +405,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
}
}
protected void handleKeyPressed(KeyEvent event) {
}
protected void reOpenDispute() {
if (selectedDispute != null) {
selectedDispute.setIsClosed(false);
@ -712,7 +716,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
// 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 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
TableColumn<Dispute, Dispute> roleColumn = getRoleColumn();
tableView.getColumns().add(roleColumn);
TableColumn<Dispute, Dispute> stateColumn = getStateColumn();
stateColumn = getStateColumn();
tableView.getColumns().add(stateColumn);
tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId));
@ -1099,7 +1103,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
});
return column;
}
}

View file

@ -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.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.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;
public abstract class DisputeAgentView extends DisputeView {
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 {
private final MultipleHolderNameDetection multipleHolderNameDetection;
public DisputeAgentView(DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager,
KeyRing keyRing,
@ -59,8 +82,15 @@ public abstract class DisputeAgentView extends DisputeView {
tradeDetailsWindow,
accountAgeWitnessService,
useDevPrivilegeKeys);
multipleHolderNameDetection = new MultipleHolderNameDetection(disputeManager);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Life cycle
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void initialize() {
super.initialize();
@ -75,8 +105,42 @@ public abstract class DisputeAgentView extends DisputeView {
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 @@ public abstract class DisputeAgentView extends DisputeView {
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<Dispute, Dispute> getAlertColumn() {
TableColumn<Dispute, Dispute> 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<Dispute> 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);
}
}