From 7c78e9819203b3c10c5b1af82f763115b99d4355 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sat, 5 Sep 2020 01:40:35 -0500 Subject: [PATCH 1/4] Scan disputes for accounts where same user used diff. real names. Might be fraudulent traders. --- .../payment/payload/BankAccountPayload.java | 2 +- .../payload/CashDepositAccountPayload.java | 2 +- .../payload/ChaseQuickPayAccountPayload.java | 2 +- .../payload/ClearXchangeAccountPayload.java | 2 +- .../InteracETransferAccountPayload.java | 2 +- .../payload/JapanBankAccountPayload.java | 7 +- .../payload/MoneyGramAccountPayload.java | 2 +- .../payload/PayloadWithHolderName.java | 22 ++ .../payload/PopmoneyAccountPayload.java | 2 +- .../payment/payload/SepaAccountPayload.java | 3 +- .../payload/SepaInstantAccountPayload.java | 2 +- .../payment/payload/SwishAccountPayload.java | 2 +- .../USPostalMoneyOrderAccountPayload.java | 2 +- .../payload/WesternUnionAccountPayload.java | 2 +- .../support/dispute/agent/FraudDetection.java | 215 ++++++++++++++++++ desktop/src/main/java/bisq/desktop/bisq.css | 5 + .../main/support/dispute/DisputeView.java | 23 +- .../dispute/agent/DisputeAgentView.java | 60 ++++- 18 files changed, 342 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/bisq/core/payment/payload/PayloadWithHolderName.java create mode 100644 core/src/main/java/bisq/core/support/dispute/agent/FraudDetection.java 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 4132b1934a..3abfa8b2ae 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 @@ 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; 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 10c39c0023..afa97764a9 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 @@ 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; 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 b8d93695e7..a320c36679 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 @@ 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 = ""; 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 139da08889..f437d5d672 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 @@ 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 = ""; 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 f404766ad2..88b2042af8 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 @@ 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 = ""; 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 4cfd4260e6..807915b2bd 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 @@ 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; + } } 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 e5f4ffc74d..03734c68f6 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 @@ 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. 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 0000000000..25efe937f4 --- /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 be018ffb55..3a451bda61 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 @@ 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 = ""; 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 c10dacb843..59425e1bcf 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 @@ 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; 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 cb8b69cd6f..54cffcd78d 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 @@ 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 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 6c236ba130..f14eafb92e 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 @@ 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 = ""; 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 e705e032dc..96a8dec520 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 @@ 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 = ""; 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 f99b80e81f..45f33186cc 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 @@ 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. diff --git a/core/src/main/java/bisq/core/support/dispute/agent/FraudDetection.java b/core/src/main/java/bisq/core/support/dispute/agent/FraudDetection.java new file mode 100644 index 0000000000..901a739c34 --- /dev/null +++ b/core/src/main/java/bisq/core/support/dispute/agent/FraudDetection.java @@ -0,0 +1,215 @@ +/* + * 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.trade.Contract; + +import bisq.common.crypto.Hash; +import bisq.common.crypto.PubKeyRing; +import bisq.common.util.Utilities; + +import javafx.collections.ListChangeListener; + +import java.util.ArrayList; +import java.util.HashMap; +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.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class FraudDetection { + public interface Listener { + void onSuspiciousDisputeDetected(); + } + + private final DisputeManager> disputeManager; + private Map> buyerRealNameAccountByAddressMap = new HashMap<>(); + private Map> sellerRealNameAccountByAddressMap = new HashMap<>(); + @Getter + private Map> accountsUsingMultipleNames = new HashMap<>(); + private List listeners = new CopyOnWriteArrayList<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + public FraudDetection(DisputeManager> disputeManager) { + this.disputeManager = disputeManager; + + disputeManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { + c.next(); + if (c.wasAdded()) { + checkForMultipleHolderNames(); + } + }); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void checkForMultipleHolderNames() { + log.error("checkForMultipleHolderNames"); + buildRealNameAccountMaps(); + detectUsageOfDifferentUserNames(); + log.error("hasSuspiciousDisputeDetected() " + hasSuspiciousDisputeDetected()); + } + + public boolean hasSuspiciousDisputeDetected() { + return !accountsUsingMultipleNames.isEmpty(); + } + + public String getAccountsUsingMultipleNamesAsString() { + return accountsUsingMultipleNames.entrySet().stream() + .map(entry -> { + String pubKeyHash = entry.getKey(); + String accountInfo = entry.getValue().stream() + .map(info -> { + String tradeId = info.getDispute().getShortTradeId(); + String holderName = info.getPayloadWithHolderName().getHolderName(); + return " Account owner name: '" + holderName + + "'; Trade ID: '" + tradeId + + "'; Address: '" + info.getAddress() + + "'; Payment method: '" + Res.get(info.getPaymentAccountPayload().getPaymentMethodId()) + + "'; Role: " + (info.isBuyer() ? "'Buyer'" : "'Seller'"); + + }) + .collect(Collectors.joining("\n")); + return "Trader with multiple identities:\n" + + accountInfo; + }) + .collect(Collectors.joining("\n\n")); + } + + public void addListener(Listener listener) { + listeners.add(listener); + } + + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void buildRealNameAccountMaps() { + buyerRealNameAccountByAddressMap.clear(); + sellerRealNameAccountByAddressMap.clear(); + disputeManager.getDisputesAsObservableList() + .forEach(dispute -> { + Contract contract = dispute.getContract(); + PubKeyRing traderPubKeyRing = dispute.getTraderPubKeyRing(); + String traderPubKeyHash = getTraderPuKeyHash(traderPubKeyRing); + String buyerPubKeyHash = getTraderPuKeyHash(contract.getBuyerPubKeyRing()); + boolean isBuyer = contract.isMyRoleBuyer(traderPubKeyRing); + + if (buyerPubKeyHash.equals(traderPubKeyHash)) { + PaymentAccountPayload buyerPaymentAccountPayload = contract.getBuyerPaymentAccountPayload(); + String buyersAddress = contract.getBuyerNodeAddress().getFullAddress(); + addToMap(traderPubKeyHash, buyerRealNameAccountByAddressMap, buyerPaymentAccountPayload, buyersAddress, dispute, isBuyer); + } else { + PaymentAccountPayload sellerPaymentAccountPayload = contract.getSellerPaymentAccountPayload(); + String sellerAddress = contract.getSellerNodeAddress().getFullAddress(); + addToMap(traderPubKeyHash, sellerRealNameAccountByAddressMap, sellerPaymentAccountPayload, sellerAddress, dispute, isBuyer); + } + }); + } + + private String getTraderPuKeyHash(PubKeyRing pubKeyRing) { + return Utilities.encodeToHex(Hash.getRipemd160hash(pubKeyRing.toProtoMessage().toByteArray())); + } + + private void addToMap(String pubKeyHash, Map> map, + PaymentAccountPayload paymentAccountPayload, + String address, + Dispute dispute, + boolean isBuyer) { + if (paymentAccountPayload instanceof PayloadWithHolderName) { + map.putIfAbsent(pubKeyHash, new ArrayList<>()); + RealNameAccountInfo info = new RealNameAccountInfo(address, + (PayloadWithHolderName) paymentAccountPayload, + paymentAccountPayload, + dispute, + isBuyer); + map.get(pubKeyHash).add(info); + } + } + + private void detectUsageOfDifferentUserNames() { + detectUsageOfDifferentUserNames(buyerRealNameAccountByAddressMap); + detectUsageOfDifferentUserNames(sellerRealNameAccountByAddressMap); + } + + private void detectUsageOfDifferentUserNames(Map> map) { + String previous = accountsUsingMultipleNames.toString(); + map.forEach((key, value) -> { + Set userNames = value.stream() + .map(info -> info.getPayloadWithHolderName().getHolderName()) + .collect(Collectors.toSet()); + if (userNames.size() > 1) { + accountsUsingMultipleNames.put(key, value); + } + }); + String updated = accountsUsingMultipleNames.toString(); + if (!previous.equals(updated)) { + listeners.forEach(Listener::onSuspiciousDisputeDetected); + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Static class + /////////////////////////////////////////////////////////////////////////////////////////// + + @Value + private static class RealNameAccountInfo { + private final String address; + private final PayloadWithHolderName payloadWithHolderName; + private final Dispute dispute; + private final boolean isBuyer; + private final PaymentAccountPayload paymentAccountPayload; + + RealNameAccountInfo(String address, + PayloadWithHolderName payloadWithHolderName, + PaymentAccountPayload paymentAccountPayload, + Dispute dispute, + boolean isBuyer) { + this.address = address; + this.payloadWithHolderName = payloadWithHolderName; + this.paymentAccountPayload = paymentAccountPayload; + this.dispute = dispute; + this.isBuyer = isBuyer; + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index 90e24fc0c4..114f272eda 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 6bf28f446c..901ef5ea47 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,6 +60,8 @@ import org.bitcoinj.core.Coin; import com.google.common.collect.Lists; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -72,6 +74,7 @@ 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; @@ -108,6 +111,8 @@ import lombok.Getter; import javax.annotation.Nullable; +import static bisq.desktop.util.FormBuilder.getIconForLabel; + public abstract class DisputeView extends ActivatableView { protected final DisputeManager> disputeManager; @@ -142,6 +147,7 @@ public abstract class DisputeView extends ActivatableView { private Map> disputeChatMessagesListeners = new HashMap<>(); @Nullable private ListChangeListener disputesListener; // Only set in mediation cases + protected Label alertIconLabel; /////////////////////////////////////////////////////////////////////////////////////////// @@ -180,6 +186,14 @@ public abstract class DisputeView extends ActivatableView { 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); @@ -219,7 +233,14 @@ public abstract class DisputeView extends ActivatableView { 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<>(); 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 582c461b08..f7a04d89ae 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,7 @@ package bisq.desktop.main.support.dispute.agent; import bisq.desktop.components.AutoTooltipButton; +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 +31,19 @@ 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.FraudDetection; import bisq.core.trade.TradeManager; import bisq.core.util.coin.CoinFormatter; import bisq.common.crypto.KeyRing; +import bisq.common.util.Utilities; import javafx.scene.control.Button; +import javafx.scene.control.Tooltip; -public abstract class DisputeAgentView extends DisputeView { +public abstract class DisputeAgentView extends DisputeView implements FraudDetection.Listener { + + private final FraudDetection fraudDetection; public DisputeAgentView(DisputeManager> disputeManager, KeyRing keyRing, @@ -59,6 +65,8 @@ public abstract class DisputeAgentView extends DisputeView { tradeDetailsWindow, accountAgeWitnessService, useDevPrivilegeKeys); + + fraudDetection = new FraudDetection(disputeManager); } @Override @@ -75,6 +83,25 @@ public abstract class DisputeAgentView extends DisputeView { fullReportButton.setVisible(true); fullReportButton.setManaged(true); + + fraudDetection.checkForMultipleHolderNames(); + } + + @Override + protected void activate() { + super.activate(); + + fraudDetection.addListener(this); + if (fraudDetection.hasSuspiciousDisputeDetected()) { + showAlertIcon(); + } + } + + @Override + protected void deactivate() { + super.deactivate(); + + fraudDetection.removeListener(this); } @Override @@ -101,6 +128,37 @@ public abstract class DisputeAgentView extends DisputeView { DisputeSession chatSession = getConcreteDisputeChatSession(dispute); chatView.display(chatSession, closeDisputeButton, root.widthProperty()); } + + @Override + public void onSuspiciousDisputeDetected() { + showAlertIcon(); + } + + private void showAlertIcon() { + String accountsUsingMultipleNamesList = fraudDetection.getAccountsUsingMultipleNamesAsString(); + alertIconLabel.setVisible(true); + alertIconLabel.setManaged(true); + alertIconLabel.setTooltip(new Tooltip("You have disputes where user used different " + + "real life names from the same application. Click for more details.")); + // Text below is for arbitrators only so no need to translate it + alertIconLabel.setOnMouseClicked(e -> new Popup() + .width(1100) + .warning("You have dispute cases where traders 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 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 got detected. Search with the trade ID for " + + "the dispute case for evaluating if it might be a fraudulent account. If so, please notify the " + + "developers and provide the contract json data to them so they can ban those traders.\n\n" + + accountsUsingMultipleNamesList) + .actionButtonText(Res.get("shared.copyToClipboard")) + .onAction(() -> Utilities.copyToClipboard(accountsUsingMultipleNamesList)) + .show()); + } } From 5a65b150febd4d1fb2d86d7d94f2ec175ef36537 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sat, 5 Sep 2020 07:41:27 -0500 Subject: [PATCH 2/4] Remove unused var --- .../java/bisq/core/support/dispute/agent/FraudDetection.java | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/main/java/bisq/core/support/dispute/agent/FraudDetection.java b/core/src/main/java/bisq/core/support/dispute/agent/FraudDetection.java index 901a739c34..aed9c54391 100644 --- a/core/src/main/java/bisq/core/support/dispute/agent/FraudDetection.java +++ b/core/src/main/java/bisq/core/support/dispute/agent/FraudDetection.java @@ -91,7 +91,6 @@ public class FraudDetection { public String getAccountsUsingMultipleNamesAsString() { return accountsUsingMultipleNames.entrySet().stream() .map(entry -> { - String pubKeyHash = entry.getKey(); String accountInfo = entry.getValue().stream() .map(info -> { String tradeId = info.getDispute().getShortTradeId(); From 4a4bd7cd126cd6b90770b8d1bf80a033fc655ce0 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sat, 5 Sep 2020 17:39:22 -0500 Subject: [PATCH 3/4] Add alert icon to list entries Support agent can mark a suspicious dispute as resolved so it does not show the alert icon anymore. In the full report a [ACK] got added to that dispute. --- .../support/dispute/agent/FraudDetection.java | 214 -------------- .../agent/MultipleHolderNameDetection.java | 270 ++++++++++++++++++ .../core/support/messages/ChatMessage.java | 2 +- .../main/support/dispute/DisputeView.java | 13 +- .../dispute/agent/DisputeAgentView.java | 195 ++++++++++--- 5 files changed, 440 insertions(+), 254 deletions(-) delete mode 100644 core/src/main/java/bisq/core/support/dispute/agent/FraudDetection.java create mode 100644 core/src/main/java/bisq/core/support/dispute/agent/MultipleHolderNameDetection.java diff --git a/core/src/main/java/bisq/core/support/dispute/agent/FraudDetection.java b/core/src/main/java/bisq/core/support/dispute/agent/FraudDetection.java deleted file mode 100644 index aed9c54391..0000000000 --- a/core/src/main/java/bisq/core/support/dispute/agent/FraudDetection.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * This file is part of Bisq. - * - * Bisq is free software: you can redistribute it and/or modify it - * under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or (at - * your option) any later version. - * - * Bisq is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public - * License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with Bisq. If not, see . - */ - -package bisq.core.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.trade.Contract; - -import bisq.common.crypto.Hash; -import bisq.common.crypto.PubKeyRing; -import bisq.common.util.Utilities; - -import javafx.collections.ListChangeListener; - -import java.util.ArrayList; -import java.util.HashMap; -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.Value; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class FraudDetection { - public interface Listener { - void onSuspiciousDisputeDetected(); - } - - private final DisputeManager> disputeManager; - private Map> buyerRealNameAccountByAddressMap = new HashMap<>(); - private Map> sellerRealNameAccountByAddressMap = new HashMap<>(); - @Getter - private Map> accountsUsingMultipleNames = new HashMap<>(); - private List listeners = new CopyOnWriteArrayList<>(); - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor - /////////////////////////////////////////////////////////////////////////////////////////// - - public FraudDetection(DisputeManager> disputeManager) { - this.disputeManager = disputeManager; - - disputeManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> { - c.next(); - if (c.wasAdded()) { - checkForMultipleHolderNames(); - } - }); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// - - public void checkForMultipleHolderNames() { - log.error("checkForMultipleHolderNames"); - buildRealNameAccountMaps(); - detectUsageOfDifferentUserNames(); - log.error("hasSuspiciousDisputeDetected() " + hasSuspiciousDisputeDetected()); - } - - public boolean hasSuspiciousDisputeDetected() { - return !accountsUsingMultipleNames.isEmpty(); - } - - public String getAccountsUsingMultipleNamesAsString() { - return accountsUsingMultipleNames.entrySet().stream() - .map(entry -> { - String accountInfo = entry.getValue().stream() - .map(info -> { - String tradeId = info.getDispute().getShortTradeId(); - String holderName = info.getPayloadWithHolderName().getHolderName(); - return " Account owner name: '" + holderName + - "'; Trade ID: '" + tradeId + - "'; Address: '" + info.getAddress() + - "'; Payment method: '" + Res.get(info.getPaymentAccountPayload().getPaymentMethodId()) + - "'; Role: " + (info.isBuyer() ? "'Buyer'" : "'Seller'"); - - }) - .collect(Collectors.joining("\n")); - return "Trader with multiple identities:\n" + - accountInfo; - }) - .collect(Collectors.joining("\n\n")); - } - - public void addListener(Listener listener) { - listeners.add(listener); - } - - public void removeListener(Listener listener) { - listeners.remove(listener); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Private - /////////////////////////////////////////////////////////////////////////////////////////// - - private void buildRealNameAccountMaps() { - buyerRealNameAccountByAddressMap.clear(); - sellerRealNameAccountByAddressMap.clear(); - disputeManager.getDisputesAsObservableList() - .forEach(dispute -> { - Contract contract = dispute.getContract(); - PubKeyRing traderPubKeyRing = dispute.getTraderPubKeyRing(); - String traderPubKeyHash = getTraderPuKeyHash(traderPubKeyRing); - String buyerPubKeyHash = getTraderPuKeyHash(contract.getBuyerPubKeyRing()); - boolean isBuyer = contract.isMyRoleBuyer(traderPubKeyRing); - - if (buyerPubKeyHash.equals(traderPubKeyHash)) { - PaymentAccountPayload buyerPaymentAccountPayload = contract.getBuyerPaymentAccountPayload(); - String buyersAddress = contract.getBuyerNodeAddress().getFullAddress(); - addToMap(traderPubKeyHash, buyerRealNameAccountByAddressMap, buyerPaymentAccountPayload, buyersAddress, dispute, isBuyer); - } else { - PaymentAccountPayload sellerPaymentAccountPayload = contract.getSellerPaymentAccountPayload(); - String sellerAddress = contract.getSellerNodeAddress().getFullAddress(); - addToMap(traderPubKeyHash, sellerRealNameAccountByAddressMap, sellerPaymentAccountPayload, sellerAddress, dispute, isBuyer); - } - }); - } - - private String getTraderPuKeyHash(PubKeyRing pubKeyRing) { - return Utilities.encodeToHex(Hash.getRipemd160hash(pubKeyRing.toProtoMessage().toByteArray())); - } - - private void addToMap(String pubKeyHash, Map> map, - PaymentAccountPayload paymentAccountPayload, - String address, - Dispute dispute, - boolean isBuyer) { - if (paymentAccountPayload instanceof PayloadWithHolderName) { - map.putIfAbsent(pubKeyHash, new ArrayList<>()); - RealNameAccountInfo info = new RealNameAccountInfo(address, - (PayloadWithHolderName) paymentAccountPayload, - paymentAccountPayload, - dispute, - isBuyer); - map.get(pubKeyHash).add(info); - } - } - - private void detectUsageOfDifferentUserNames() { - detectUsageOfDifferentUserNames(buyerRealNameAccountByAddressMap); - detectUsageOfDifferentUserNames(sellerRealNameAccountByAddressMap); - } - - private void detectUsageOfDifferentUserNames(Map> map) { - String previous = accountsUsingMultipleNames.toString(); - map.forEach((key, value) -> { - Set userNames = value.stream() - .map(info -> info.getPayloadWithHolderName().getHolderName()) - .collect(Collectors.toSet()); - if (userNames.size() > 1) { - accountsUsingMultipleNames.put(key, value); - } - }); - String updated = accountsUsingMultipleNames.toString(); - if (!previous.equals(updated)) { - listeners.forEach(Listener::onSuspiciousDisputeDetected); - } - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Static class - /////////////////////////////////////////////////////////////////////////////////////////// - - @Value - private static class RealNameAccountInfo { - private final String address; - private final PayloadWithHolderName payloadWithHolderName; - private final Dispute dispute; - private final boolean isBuyer; - private final PaymentAccountPayload paymentAccountPayload; - - RealNameAccountInfo(String address, - PayloadWithHolderName payloadWithHolderName, - PaymentAccountPayload paymentAccountPayload, - Dispute dispute, - boolean isBuyer) { - this.address = address; - this.payloadWithHolderName = payloadWithHolderName; - this.paymentAccountPayload = paymentAccountPayload; - this.dispute = dispute; - this.isBuyer = isBuyer; - } - } -} 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 0000000000..48025ae266 --- /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 a4e328ec39..b798a37463 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 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 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 901ef5ea47..251727e43e 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 @@ -127,7 +127,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 @@ -142,12 +142,12 @@ public abstract class DisputeView extends ActivatableView { 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; /////////////////////////////////////////////////////////////////////////////////////////// @@ -231,7 +231,7 @@ public abstract class DisputeView extends ActivatableView { Pane spacer = new Pane(); HBox.setHgrow(spacer, Priority.ALWAYS); - filterBox = new HBox(); + HBox filterBox = new HBox(); filterBox.setSpacing(5); filterBox.getChildren().addAll(label, filterTextField, @@ -419,7 +419,7 @@ public abstract class DisputeView extends ActivatableView { } } - protected void handleKeyPressed(KeyEvent event) { + private void handleKeyPressed(KeyEvent event) { } protected void reOpenDispute() { @@ -733,7 +733,7 @@ public abstract class DisputeView extends ActivatableView { // Table /////////////////////////////////////////////////////////////////////////////////////////// - private void setupTable() { + protected void setupTable() { tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); Label placeholder = new AutoTooltipLabel(Res.get("support.noTickets")); placeholder.setWrapText(true); @@ -764,7 +764,7 @@ public abstract class DisputeView extends ActivatableView { TableColumn roleColumn = getRoleColumn(); tableView.getColumns().add(roleColumn); - TableColumn stateColumn = getStateColumn(); + stateColumn = getStateColumn(); tableView.getColumns().add(stateColumn); tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId)); @@ -1120,7 +1120,6 @@ public abstract class DisputeView extends ActivatableView { }); 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 f7a04d89ae..13e5260867 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,7 @@ 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; @@ -31,19 +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.FraudDetection; +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 implements FraudDetection.Listener { +import javafx.geometry.Insets; - private final FraudDetection fraudDetection; +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> disputeManager, KeyRing keyRing, @@ -66,9 +83,14 @@ public abstract class DisputeAgentView extends DisputeView implements FraudDetec accountAgeWitnessService, useDevPrivilegeKeys); - fraudDetection = new FraudDetection(disputeManager); + multipleHolderNameDetection = new MultipleHolderNameDetection(disputeManager); } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Life cycle + /////////////////////////////////////////////////////////////////////////////////////////// + @Override public void initialize() { super.initialize(); @@ -84,16 +106,16 @@ public abstract class DisputeAgentView extends DisputeView implements FraudDetec fullReportButton.setVisible(true); fullReportButton.setManaged(true); - fraudDetection.checkForMultipleHolderNames(); + multipleHolderNameDetection.detectMultipleHolderNames(); } @Override protected void activate() { super.activate(); - fraudDetection.addListener(this); - if (fraudDetection.hasSuspiciousDisputeDetected()) { - showAlertIcon(); + multipleHolderNameDetection.addListener(this); + if (multipleHolderNameDetection.hasSuspiciousDisputesDetected()) { + suspiciousDisputeDetected(); } } @@ -101,9 +123,24 @@ public abstract class DisputeAgentView extends DisputeView implements FraudDetec protected void deactivate() { super.deactivate(); - fraudDetection.removeListener(this); + multipleHolderNameDetection.removeListener(this); } + + /////////////////////////////////////////////////////////////////////////////////////////// + // MultipleHolderNamesDetection.Listener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onSuspiciousDisputeDetected() { + suspiciousDisputeDetected(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DisputeView + /////////////////////////////////////////////////////////////////////////////////////////// + @Override protected void applyFilteredListPredicate(String filterString) { filteredList.setPredicate(dispute -> { @@ -130,34 +167,128 @@ public abstract class DisputeAgentView extends DisputeView implements FraudDetec } @Override - public void onSuspiciousDisputeDetected() { - showAlertIcon(); + protected void setupTable() { + super.setupTable(); + + stateColumn.getStyleClass().remove("last-column"); + tableView.getColumns().add(getAlertColumn()); } - private void showAlertIcon() { - String accountsUsingMultipleNamesList = fraudDetection.getAccountsUsingMultipleNamesAsString(); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void suspiciousDisputeDetected() { alertIconLabel.setVisible(true); alertIconLabel.setManaged(true); - alertIconLabel.setTooltip(new Tooltip("You have disputes where user used different " + - "real life names from the same application. Click for more details.")); + 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 -> new Popup() - .width(1100) - .warning("You have dispute cases where traders 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 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 got detected. Search with the trade ID for " + - "the dispute case for evaluating if it might be a fraudulent account. If so, please notify the " + - "developers and provide the contract json data to them so they can ban those traders.\n\n" + - accountsUsingMultipleNamesList) - .actionButtonText(Res.get("shared.copyToClipboard")) - .onAction(() -> Utilities.copyToClipboard(accountsUsingMultipleNamesList)) - .show()); + 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); } } From 959009d6d66cdcf3cdd5fa5702cac6fe15330708 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Sat, 5 Sep 2020 17:54:32 -0500 Subject: [PATCH 4/4] Remove unused method --- .../main/support/dispute/DisputeView.java | 17 ----------------- 1 file changed, 17 deletions(-) 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 251727e43e..fcba3c3b65 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 @@ -62,14 +62,12 @@ import com.google.common.collect.Lists; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; -import javafx.scene.Scene; 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; @@ -85,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; @@ -137,8 +133,6 @@ 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; @@ -253,8 +247,6 @@ public abstract class DisputeView extends ActivatableView { selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> chatView.setInputBoxVisible(!newValue); - keyEventEventHandler = this::handleKeyPressed; - chatView = new ChatView(disputeManager, formatter); chatView.initialize(); } @@ -284,9 +276,6 @@ public abstract class DisputeView extends ActivatableView { 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) @@ -345,9 +334,6 @@ public abstract class DisputeView extends ActivatableView { selectedDisputeSubscription.unsubscribe(); removeListenersOnSelectDispute(); - if (scene != null) - scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler); - if (chatView != null) chatView.deactivate(); } @@ -419,9 +405,6 @@ public abstract class DisputeView extends ActivatableView { } } - private void handleKeyPressed(KeyEvent event) { - } - protected void reOpenDispute() { if (selectedDispute != null) { selectedDispute.setIsClosed(false);