mirror of
https://github.com/bisq-network/bisq.git
synced 2025-02-24 15:10:44 +01:00
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.
This commit is contained in:
parent
5a65b150fe
commit
4a4bd7cd12
5 changed files with 440 additions and 254 deletions
|
@ -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 <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.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<? extends DisputeList<? extends DisputeList>> disputeManager;
|
||||
private Map<String, List<RealNameAccountInfo>> buyerRealNameAccountByAddressMap = new HashMap<>();
|
||||
private Map<String, List<RealNameAccountInfo>> sellerRealNameAccountByAddressMap = new HashMap<>();
|
||||
@Getter
|
||||
private Map<String, List<RealNameAccountInfo>> accountsUsingMultipleNames = new HashMap<>();
|
||||
private List<Listener> listeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public FraudDetection(DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager) {
|
||||
this.disputeManager = disputeManager;
|
||||
|
||||
disputeManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) 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<String, List<RealNameAccountInfo>> 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<String, List<RealNameAccountInfo>> map) {
|
||||
String previous = accountsUsingMultipleNames.toString();
|
||||
map.forEach((key, value) -> {
|
||||
Set<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -127,7 +127,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
|
||||
|
@ -142,12 +142,12 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
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;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -231,7 +231,7 @@ 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,
|
||||
|
@ -419,7 +419,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
}
|
||||
}
|
||||
|
||||
protected void handleKeyPressed(KeyEvent event) {
|
||||
private void handleKeyPressed(KeyEvent event) {
|
||||
}
|
||||
|
||||
protected void reOpenDispute() {
|
||||
|
@ -733,7 +733,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);
|
||||
|
@ -764,7 +764,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));
|
||||
|
@ -1120,7 +1120,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
});
|
||||
return column;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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<? extends DisputeList<? extends DisputeList>> 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<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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue