mirror of
https://github.com/bisq-network/bisq.git
synced 2025-02-24 15:10:44 +01:00
Merge pull request #4484 from chimp1984/detect-accounts-with-diff-real-names
Scan disputes for accounts where same user used diff. real names.
This commit is contained in:
commit
c188284d0b
19 changed files with 534 additions and 39 deletions
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 = "";
|
||||
|
||||
|
|
|
@ -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 = "";
|
||||
|
||||
|
|
|
@ -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 = "";
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.payment.payload;
|
||||
|
||||
public interface PayloadWithHolderName {
|
||||
String getHolderName();
|
||||
}
|
|
@ -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 = "";
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = "";
|
||||
|
||||
|
|
|
@ -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 = "";
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -60,18 +60,19 @@ import org.bitcoinj.core.Coin;
|
|||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import javafx.scene.Scene;
|
||||
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
|
||||
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
|
||||
|
@ -82,8 +83,6 @@ import javafx.beans.property.ReadOnlyBooleanProperty;
|
|||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
|
||||
import javafx.event.EventHandler;
|
||||
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
|
@ -108,6 +107,8 @@ import lombok.Getter;
|
|||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static bisq.desktop.util.FormBuilder.getIconForLabel;
|
||||
|
||||
public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
||||
|
||||
protected final DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager;
|
||||
|
@ -122,7 +123,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
private final AccountAgeWitnessService accountAgeWitnessService;
|
||||
private final boolean useDevPrivilegeKeys;
|
||||
|
||||
private TableView<Dispute> tableView;
|
||||
protected TableView<Dispute> tableView;
|
||||
private SortedList<Dispute> sortedList;
|
||||
|
||||
@Getter
|
||||
|
@ -132,16 +133,15 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
|
||||
private ChangeListener<Boolean> selectedDisputeClosedPropertyListener;
|
||||
private Subscription selectedDisputeSubscription;
|
||||
private EventHandler<KeyEvent> keyEventEventHandler;
|
||||
private Scene scene;
|
||||
protected FilteredList<Dispute> filteredList;
|
||||
protected InputTextField filterTextField;
|
||||
private ChangeListener<String> filterTextFieldListener;
|
||||
private HBox filterBox;
|
||||
protected AutoTooltipButton reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton;
|
||||
private Map<String, ListChangeListener<ChatMessage>> disputeChatMessagesListeners = new HashMap<>();
|
||||
@Nullable
|
||||
private ListChangeListener<Dispute> disputesListener; // Only set in mediation cases
|
||||
protected Label alertIconLabel;
|
||||
protected TableColumn<Dispute, Dispute> stateColumn;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -180,6 +180,14 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText());
|
||||
HBox.setHgrow(filterTextField, Priority.NEVER);
|
||||
|
||||
alertIconLabel = new Label();
|
||||
Text icon = getIconForLabel(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, "2em", alertIconLabel);
|
||||
icon.getStyleClass().add("alert-icon");
|
||||
HBox.setMargin(alertIconLabel, new Insets(4, 0, 0, 10));
|
||||
alertIconLabel.setMouseTransparent(false);
|
||||
alertIconLabel.setVisible(false);
|
||||
alertIconLabel.setManaged(false);
|
||||
|
||||
reOpenButton = new AutoTooltipButton(Res.get("support.reOpenButton.label"));
|
||||
reOpenButton.setDisable(true);
|
||||
reOpenButton.setVisible(false);
|
||||
|
@ -217,9 +225,16 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
Pane spacer = new Pane();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
|
||||
filterBox = new HBox();
|
||||
HBox filterBox = new HBox();
|
||||
filterBox.setSpacing(5);
|
||||
filterBox.getChildren().addAll(label, filterTextField, spacer, reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton);
|
||||
filterBox.getChildren().addAll(label,
|
||||
filterTextField,
|
||||
alertIconLabel,
|
||||
spacer,
|
||||
reOpenButton,
|
||||
sendPrivateNotificationButton,
|
||||
reportButton,
|
||||
fullReportButton);
|
||||
VBox.setVgrow(filterBox, Priority.NEVER);
|
||||
|
||||
tableView = new TableView<>();
|
||||
|
@ -232,8 +247,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
|
||||
selectedDisputeClosedPropertyListener = (observable, oldValue, newValue) -> chatView.setInputBoxVisible(!newValue);
|
||||
|
||||
keyEventEventHandler = this::handleKeyPressed;
|
||||
|
||||
chatView = new ChatView(disputeManager, formatter);
|
||||
chatView.initialize();
|
||||
}
|
||||
|
@ -263,9 +276,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
chatView.scrollToBottom();
|
||||
}
|
||||
|
||||
scene = root.getScene();
|
||||
if (scene != null)
|
||||
scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler);
|
||||
|
||||
// If doPrint=true we print out a html page which opens tabs with all deposit txs
|
||||
// (firefox needs about:config change to allow > 20 tabs)
|
||||
|
@ -324,9 +334,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
selectedDisputeSubscription.unsubscribe();
|
||||
removeListenersOnSelectDispute();
|
||||
|
||||
if (scene != null)
|
||||
scene.removeEventHandler(KeyEvent.KEY_RELEASED, keyEventEventHandler);
|
||||
|
||||
if (chatView != null)
|
||||
chatView.deactivate();
|
||||
}
|
||||
|
@ -398,9 +405,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
}
|
||||
}
|
||||
|
||||
protected void handleKeyPressed(KeyEvent event) {
|
||||
}
|
||||
|
||||
protected void reOpenDispute() {
|
||||
if (selectedDispute != null) {
|
||||
selectedDispute.setIsClosed(false);
|
||||
|
@ -712,7 +716,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
// Table
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void setupTable() {
|
||||
protected void setupTable() {
|
||||
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||
Label placeholder = new AutoTooltipLabel(Res.get("support.noTickets"));
|
||||
placeholder.setWrapText(true);
|
||||
|
@ -743,7 +747,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
TableColumn<Dispute, Dispute> roleColumn = getRoleColumn();
|
||||
tableView.getColumns().add(roleColumn);
|
||||
|
||||
TableColumn<Dispute, Dispute> stateColumn = getStateColumn();
|
||||
stateColumn = getStateColumn();
|
||||
tableView.getColumns().add(stateColumn);
|
||||
|
||||
tradeIdColumn.setComparator(Comparator.comparing(Dispute::getTradeId));
|
||||
|
@ -1099,7 +1103,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
});
|
||||
return column;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
package bisq.desktop.main.support.dispute.agent;
|
||||
|
||||
import bisq.desktop.components.AutoTooltipButton;
|
||||
import bisq.desktop.components.AutoTooltipTableColumn;
|
||||
import bisq.desktop.main.overlays.popups.Popup;
|
||||
import bisq.desktop.main.overlays.windows.ContractWindow;
|
||||
import bisq.desktop.main.overlays.windows.DisputeSummaryWindow;
|
||||
import bisq.desktop.main.overlays.windows.TradeDetailsWindow;
|
||||
|
@ -30,14 +32,35 @@ import bisq.core.support.dispute.Dispute;
|
|||
import bisq.core.support.dispute.DisputeList;
|
||||
import bisq.core.support.dispute.DisputeManager;
|
||||
import bisq.core.support.dispute.DisputeSession;
|
||||
import bisq.core.support.dispute.agent.MultipleHolderNameDetection;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.DontShowAgainLookup;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
|
||||
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TableCell;
|
||||
import javafx.scene.control.TableColumn;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.text.Text;
|
||||
|
||||
public abstract class DisputeAgentView extends DisputeView {
|
||||
import javafx.geometry.Insets;
|
||||
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static bisq.desktop.util.FormBuilder.getIconForLabel;
|
||||
|
||||
public abstract class DisputeAgentView extends DisputeView implements MultipleHolderNameDetection.Listener {
|
||||
|
||||
private final MultipleHolderNameDetection multipleHolderNameDetection;
|
||||
|
||||
public DisputeAgentView(DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager,
|
||||
KeyRing keyRing,
|
||||
|
@ -59,8 +82,15 @@ public abstract class DisputeAgentView extends DisputeView {
|
|||
tradeDetailsWindow,
|
||||
accountAgeWitnessService,
|
||||
useDevPrivilegeKeys);
|
||||
|
||||
multipleHolderNameDetection = new MultipleHolderNameDetection(disputeManager);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Life cycle
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
super.initialize();
|
||||
|
@ -75,8 +105,42 @@ public abstract class DisputeAgentView extends DisputeView {
|
|||
|
||||
fullReportButton.setVisible(true);
|
||||
fullReportButton.setManaged(true);
|
||||
|
||||
multipleHolderNameDetection.detectMultipleHolderNames();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void activate() {
|
||||
super.activate();
|
||||
|
||||
multipleHolderNameDetection.addListener(this);
|
||||
if (multipleHolderNameDetection.hasSuspiciousDisputesDetected()) {
|
||||
suspiciousDisputeDetected();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deactivate() {
|
||||
super.deactivate();
|
||||
|
||||
multipleHolderNameDetection.removeListener(this);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// MultipleHolderNamesDetection.Listener
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onSuspiciousDisputeDetected() {
|
||||
suspiciousDisputeDetected();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// DisputeView
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected void applyFilteredListPredicate(String filterString) {
|
||||
filteredList.setPredicate(dispute -> {
|
||||
|
@ -101,6 +165,131 @@ public abstract class DisputeAgentView extends DisputeView {
|
|||
DisputeSession chatSession = getConcreteDisputeChatSession(dispute);
|
||||
chatView.display(chatSession, closeDisputeButton, root.widthProperty());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setupTable() {
|
||||
super.setupTable();
|
||||
|
||||
stateColumn.getStyleClass().remove("last-column");
|
||||
tableView.getColumns().add(getAlertColumn());
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void suspiciousDisputeDetected() {
|
||||
alertIconLabel.setVisible(true);
|
||||
alertIconLabel.setManaged(true);
|
||||
alertIconLabel.setTooltip(new Tooltip("You have suspicious disputes where the same trader used different " +
|
||||
"account holder names.\nClick for more information."));
|
||||
// Text below is for arbitrators only so no need to translate it
|
||||
alertIconLabel.setOnMouseClicked(e -> {
|
||||
String reportForAllDisputes = multipleHolderNameDetection.getReportForAllDisputes();
|
||||
new Popup()
|
||||
.width(1100)
|
||||
.warning(getReportMessage(reportForAllDisputes, "traders"))
|
||||
.actionButtonText(Res.get("shared.copyToClipboard"))
|
||||
.onAction(() -> Utilities.copyToClipboard(reportForAllDisputes))
|
||||
.show();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private TableColumn<Dispute, Dispute> getAlertColumn() {
|
||||
TableColumn<Dispute, Dispute> column = new AutoTooltipTableColumn<>("Alert") {
|
||||
{
|
||||
setMinWidth(50);
|
||||
}
|
||||
};
|
||||
column.getStyleClass().add("last-column");
|
||||
column.setCellValueFactory((dispute) -> new ReadOnlyObjectWrapper<>(dispute.getValue()));
|
||||
column.setCellFactory(
|
||||
c -> new TableCell<>() {
|
||||
Label alertIconLabel;
|
||||
|
||||
@Override
|
||||
public void updateItem(Dispute dispute, boolean empty) {
|
||||
if (dispute != null && !empty) {
|
||||
if (!showAlertAtDispute(dispute)) {
|
||||
setGraphic(null);
|
||||
if (alertIconLabel != null) {
|
||||
alertIconLabel.setOnMouseClicked(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (alertIconLabel != null) {
|
||||
alertIconLabel.setOnMouseClicked(null);
|
||||
}
|
||||
|
||||
alertIconLabel = new Label();
|
||||
Text icon = getIconForLabel(MaterialDesignIcon.ALERT_CIRCLE_OUTLINE, "1.5em", alertIconLabel);
|
||||
icon.getStyleClass().add("alert-icon");
|
||||
HBox.setMargin(alertIconLabel, new Insets(4, 0, 0, 10));
|
||||
alertIconLabel.setMouseTransparent(false);
|
||||
setGraphic(alertIconLabel);
|
||||
|
||||
alertIconLabel.setOnMouseClicked(e -> {
|
||||
List<Dispute> realNameAccountInfoList = multipleHolderNameDetection.getDisputesForTrader(dispute);
|
||||
String reportForDisputeOfTrader = multipleHolderNameDetection.getReportForDisputeOfTrader(realNameAccountInfoList);
|
||||
String key = MultipleHolderNameDetection.getAckKey(dispute);
|
||||
new Popup()
|
||||
.width(1100)
|
||||
.warning(getReportMessage(reportForDisputeOfTrader, "this trader"))
|
||||
.actionButtonText(Res.get("shared.copyToClipboard"))
|
||||
.onAction(() -> {
|
||||
Utilities.copyToClipboard(reportForDisputeOfTrader);
|
||||
if (!DontShowAgainLookup.showAgain(key)) {
|
||||
setGraphic(null);
|
||||
}
|
||||
})
|
||||
.dontShowAgainId(key)
|
||||
.dontShowAgainText("Is not suspicious")
|
||||
.onClose(() -> {
|
||||
if (!DontShowAgainLookup.showAgain(key)) {
|
||||
setGraphic(null);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
});
|
||||
} else {
|
||||
setGraphic(null);
|
||||
if (alertIconLabel != null) {
|
||||
alertIconLabel.setOnMouseClicked(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
column.setComparator((o1, o2) -> Boolean.compare(showAlertAtDispute(o1), showAlertAtDispute(o2)));
|
||||
column.setSortable(true);
|
||||
return column;
|
||||
}
|
||||
|
||||
private boolean showAlertAtDispute(Dispute dispute) {
|
||||
return DontShowAgainLookup.showAgain(MultipleHolderNameDetection.getAckKey(dispute)) &&
|
||||
!multipleHolderNameDetection.getDisputesForTrader(dispute).isEmpty();
|
||||
}
|
||||
|
||||
private String getReportMessage(String report, String subString) {
|
||||
return "You have dispute cases where " + subString + " used different account holder names.\n\n" +
|
||||
"This might be not critical in case of small variations of the same name " +
|
||||
"(e.g. first name and last name are swapped), " +
|
||||
"but if the name is completely different you should request information from the trader why they " +
|
||||
"used a different name and request proof that the person with the real name is aware " +
|
||||
"of the trade. " +
|
||||
"It can be that the trader uses the account of their wife/husband, but it also could " +
|
||||
"be a case of a stolen bank account or money laundering.\n\n" +
|
||||
"Please check below the list of the names which have been detected. " +
|
||||
"Search with the trade ID for the dispute case or check out the alert icon at each dispute in " +
|
||||
"the list (you might need to remove the 'open' filter) and evaluate " +
|
||||
"if it might be a fraudulent account (buyer role is more likely to be fraudulent). " +
|
||||
"If you find suspicious disputes, please notify the developers and provide the contract json data " +
|
||||
"to them so they can ban those traders.\n\n" +
|
||||
Utilities.toTruncatedString(report, 700, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue