diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index 2bafa44b85..164f74a04d 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -114,6 +114,7 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; import ch.qos.logback.classic.Level; @@ -851,7 +852,13 @@ public class BisqSetup { priceAlert.onAllServicesInitialized(); marketAlerts.onAllServicesInitialized(); - user.onAllServicesInitialized(revolutAccountsUpdateHandler); + if (revolutAccountsUpdateHandler != null) { + revolutAccountsUpdateHandler.accept(user.getPaymentAccountsAsObservable().stream() + .filter(paymentAccount -> paymentAccount instanceof RevolutAccount) + .map(paymentAccount -> (RevolutAccount) paymentAccount) + .filter(RevolutAccount::userNameNotSet) + .collect(Collectors.toList())); + } allBasicServicesInitialized = true; } diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java index d4fe695151..08a96db151 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccountUtil.java @@ -20,7 +20,9 @@ package bisq.core.payment; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.locale.Country; import bisq.core.offer.Offer; +import bisq.core.payment.payload.PaymentAccountPayload; import bisq.core.payment.payload.PaymentMethod; +import bisq.core.user.User; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -136,6 +138,13 @@ public class PaymentAccountUtil { paymentAccount != null && paymentAccount.getPaymentMethod().equals(PaymentMethod.BLOCK_CHAINS_INSTANT)); } + public static Optional findPaymentAccount(PaymentAccountPayload paymentAccountPayload, + User user) { + return user.getPaymentAccountsAsObservable().stream(). + filter(e -> e.getPaymentAccountPayload().equals(paymentAccountPayload)) + .findAny(); + } + // TODO no code duplication found in UI code (added for API) // That is optional and set to null if not supported (AltCoins,...) /* public static String getCountryCode(PaymentAccount paymentAccount) { diff --git a/core/src/main/java/bisq/core/payment/RevolutAccount.java b/core/src/main/java/bisq/core/payment/RevolutAccount.java index 07282769f6..9e8d41496f 100644 --- a/core/src/main/java/bisq/core/payment/RevolutAccount.java +++ b/core/src/main/java/bisq/core/payment/RevolutAccount.java @@ -44,7 +44,15 @@ public final class RevolutAccount extends PaymentAccount { return ((RevolutAccountPayload) paymentAccountPayload).getUserName(); } + public String getAccountId() { + return ((RevolutAccountPayload) paymentAccountPayload).getAccountId(); + } + public boolean userNameNotSet() { return ((RevolutAccountPayload) paymentAccountPayload).userNameNotSet(); } + + public boolean hasOldAccountId() { + return ((RevolutAccountPayload) paymentAccountPayload).hasOldAccountId(); + } } 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/RevolutAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java index 7c8926ab10..72548a14d4 100644 --- a/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java +++ b/core/src/main/java/bisq/core/payment/payload/RevolutAccountPayload.java @@ -19,7 +19,8 @@ package bisq.core.payment.payload; import bisq.core.locale.Res; -import bisq.common.proto.ProtoUtil; +import bisq.common.util.JsonExclude; +import bisq.common.util.Tuple2; import com.google.protobuf.Message; @@ -27,19 +28,23 @@ import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; +import static com.google.common.base.Preconditions.checkArgument; + @EqualsAndHashCode(callSuper = true) @ToString @Slf4j public final class RevolutAccountPayload extends PaymentAccountPayload { - // Not used anymore from outside. Only used as internal Id to not break existing account witness objects + // Only used as internal Id to not break existing account witness objects + // We still show it in case it is different to the userName for additional security + @Getter private String accountId = ""; // Was added in 1.3.8 @@ -47,8 +52,13 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { // Old accounts get a popup to add the new required field userName but accountId is // left unchanged. Newly created accounts fill accountId with the value of userName. // In the UI we only use userName. - @Nullable - private String userName = null; + + // For backward compatibility we need to exclude the new field for the contract json. + // We can remove that after a while when risk that users with pre 1.3.8 version trade with updated + // users is very low. + @JsonExclude + @Getter + private String userName = ""; public RevolutAccountPayload(String paymentMethod, String id) { super(paymentMethod, id); @@ -71,14 +81,14 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { excludeFromJsonDataMap); this.accountId = accountId; - this.userName = userName; + setUserName(userName); } @Override public Message toProtoMessage() { protobuf.RevolutAccountPayload.Builder revolutBuilder = protobuf.RevolutAccountPayload.newBuilder() - .setAccountId(accountId); - Optional.ofNullable(userName).ifPresent(revolutBuilder::setUserName); + .setAccountId(accountId) + .setUserName(userName); return getPaymentAccountPayloadBuilder().setRevolutAccountPayload(revolutBuilder).build(); } @@ -88,7 +98,7 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { return new RevolutAccountPayload(proto.getPaymentMethodId(), proto.getId(), revolutAccountPayload.getAccountId(), - ProtoUtil.stringOrNullFromProto(revolutAccountPayload.getUserName()), + revolutAccountPayload.getUserName(), proto.getMaxTradePeriod(), new HashMap<>(proto.getExcludeFromJsonDataMap())); } @@ -100,7 +110,34 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { @Override public String getPaymentDetails() { - return Res.get(paymentMethodId) + " - " + Res.getWithCol("payment.account.userName") + " " + getUserName(); + Tuple2 tuple = getLabelValueTuple(); + return Res.get(paymentMethodId) + " - " + tuple.first + ": " + tuple.second; + } + + private Tuple2 getLabelValueTuple() { + String label; + String value; + checkArgument(!userName.isEmpty() || hasOldAccountId(), + "Either username must be set or we have an old account with accountId"); + if (!userName.isEmpty()) { + label = Res.get("payment.account.userName"); + value = userName; + + if (hasOldAccountId()) { + label += "/" + Res.get("payment.account.phoneNr"); + value += "/" + accountId; + } + } else { + label = Res.get("payment.account.phoneNr"); + value = accountId; + } + return new Tuple2<>(label, value); + } + + public Tuple2 getRecipientsAccountData() { + Tuple2 tuple = getLabelValueTuple(); + String label = Res.get("portfolio.pending.step2_buyer.recipientsAccountData", tuple.first); + return new Tuple2<>(label, tuple.second); } @Override @@ -111,23 +148,30 @@ public final class RevolutAccountPayload extends PaymentAccountPayload { @Override public byte[] getAgeWitnessInputData() { // getAgeWitnessInputData is called at new account creation when accountId is empty string. - return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); + if (hasOldAccountId()) { + // If the accountId was already in place (updated user who had used accountId for account age) we keep the + // old accountId to not invalidate the existing account age witness. + return super.getAgeWitnessInputData(accountId.getBytes(StandardCharsets.UTF_8)); + + } else { + // If a new account was registered from version 1.3.8 or later we use the userName. + return super.getAgeWitnessInputData(userName.getBytes(StandardCharsets.UTF_8)); + } } - public void setUserName(@Nullable String userName) { + public boolean userNameNotSet() { + return userName.isEmpty(); + } + + public boolean hasOldAccountId() { + return !accountId.equals(userName); + } + + public void setUserName(String userName) { this.userName = userName; - // We only set accountId to userName for new accounts. Existing accounts have accountId set with email - // or phone nr. and we keep that to not break account signing. + // We need to set accountId as pre v1.3.8 clients expect the accountId field if (accountId.isEmpty()) { accountId = userName; } } - - public String getUserName() { - return userName != null ? userName : accountId; - } - - public boolean userNameNotSet() { - return userName == null; - } } 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/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/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java index 99d5f670d3..4c939f3972 100644 --- a/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java +++ b/core/src/main/java/bisq/core/trade/txproof/xmr/XmrTxProofService.java @@ -82,6 +82,8 @@ public class XmrTxProofService implements AssetTxProofService { private Map> tradeStateListenerMap = new HashMap<>(); private ChangeListener btcPeersListener, btcBlockListener; private BootstrapListener bootstrapListener; + private MonadicBinding p2pNetworkAndWalletReady; + private ChangeListener p2pNetworkAndWalletReadyListener; /////////////////////////////////////////////////////////////////////////////////////////// @@ -122,18 +124,26 @@ public class XmrTxProofService implements AssetTxProofService { // As we might trigger the payout tx we want to be sure that we are well connected to the Bitcoin network. // onAllServicesInitialized is called once we have received the initial data but we want to have our // hidden service published and upDatedDataResponse received before we start. - MonadicBinding p2pNetworkAndWalletReady = EasyBind.combine(isP2pBootstrapped(), hasSufficientBtcPeers(), isBtcBlockDownloadComplete(), - (isP2pBootstrapped, hasSufficientBtcPeers, isBtcBlockDownloadComplete) -> { - log.info("isP2pBootstrapped={}, hasSufficientBtcPeers={} isBtcBlockDownloadComplete={}", - isP2pBootstrapped, hasSufficientBtcPeers, isBtcBlockDownloadComplete); - return isP2pBootstrapped && hasSufficientBtcPeers && isBtcBlockDownloadComplete; - }); + BooleanProperty isP2pBootstrapped = isP2pBootstrapped(); + BooleanProperty hasSufficientBtcPeers = hasSufficientBtcPeers(); + BooleanProperty isBtcBlockDownloadComplete = isBtcBlockDownloadComplete(); + if (isP2pBootstrapped.get() && hasSufficientBtcPeers.get() && isBtcBlockDownloadComplete.get()) { + onP2pNetworkAndWalletReady(); + } else { + p2pNetworkAndWalletReady = EasyBind.combine(isP2pBootstrapped, hasSufficientBtcPeers, isBtcBlockDownloadComplete, + (bootstrapped, sufficientPeers, downloadComplete) -> { + log.info("isP2pBootstrapped={}, hasSufficientBtcPeers={} isBtcBlockDownloadComplete={}", + bootstrapped, sufficientPeers, downloadComplete); + return bootstrapped && sufficientPeers && downloadComplete; + }); - p2pNetworkAndWalletReady.subscribe((observable, oldValue, newValue) -> { - if (newValue) { - onP2pNetworkAndWalletReady(); - } - }); + p2pNetworkAndWalletReadyListener = (observable, oldValue, newValue) -> { + if (newValue) { + onP2pNetworkAndWalletReady(); + } + }; + p2pNetworkAndWalletReady.subscribe(p2pNetworkAndWalletReadyListener); + } } @Override @@ -148,6 +158,12 @@ public class XmrTxProofService implements AssetTxProofService { /////////////////////////////////////////////////////////////////////////////////////////// private void onP2pNetworkAndWalletReady() { + if (p2pNetworkAndWalletReady != null) { + p2pNetworkAndWalletReady.removeListener(p2pNetworkAndWalletReadyListener); + p2pNetworkAndWalletReady = null; + p2pNetworkAndWalletReadyListener = null; + } + if (!preferences.findAutoConfirmSettings("XMR").isPresent()) { log.error("AutoConfirmSettings is not present"); } diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index f4ef6285e0..4ae7eba640 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -123,12 +123,14 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid new BlockChainExplorer("bsq.bisq.cc (@m52go)", "https://bsq.bisq.cc/tx.html?tx=", "https://bsq.bisq.cc/Address.html?addr=") )); - //TODO add a second before release private static final ArrayList XMR_TX_PROOF_SERVICES_CLEAR_NET = new ArrayList<>(Arrays.asList( - "78.47.61.90:8081")); - //TODO add a second before release + "78.47.61.90:8081", // @emzy + "node77.monero.wiz.biz" // @wiz + )); private static final ArrayList XMR_TX_PROOF_SERVICES = new ArrayList<>(Arrays.asList( - "monero3bec7m26vx6si6qo7q7imlaoz45ot5m2b5z2ppgoooo6jx2rqd.onion")); + "monero3bec7m26vx6si6qo7q7imlaoz45ot5m2b5z2ppgoooo6jx2rqd.onion", // @emzy + "wizxmr4hbdxdszqm5rfyqvceyca5jq62ppvtuznasnk66wvhhvgm3uyd.onion" // @wiz + )); public static final boolean USE_SYMMETRIC_SECURITY_DEPOSIT = true; diff --git a/core/src/main/java/bisq/core/user/User.java b/core/src/main/java/bisq/core/user/User.java index 0f6957c83e..75891395d2 100644 --- a/core/src/main/java/bisq/core/user/User.java +++ b/core/src/main/java/bisq/core/user/User.java @@ -24,7 +24,6 @@ import bisq.core.locale.TradeCurrency; import bisq.core.notifications.alerts.market.MarketAlertFilter; import bisq.core.notifications.alerts.price.PriceAlertFilter; import bisq.core.payment.PaymentAccount; -import bisq.core.payment.RevolutAccount; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.mediation.mediator.Mediator; import bisq.core.support.dispute.refund.refundagent.RefundAgent; @@ -51,7 +50,6 @@ import java.util.HashSet; import java.util.List; import java.util.Optional; import java.util.Set; -import java.util.function.Consumer; import java.util.stream.Collectors; import lombok.AllArgsConstructor; @@ -128,16 +126,6 @@ public class User implements PersistedDataHost { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void onAllServicesInitialized(@Nullable Consumer> resultHandler) { - if (resultHandler != null) { - resultHandler.accept(paymentAccountsAsObservable.stream() - .filter(paymentAccount -> paymentAccount instanceof RevolutAccount) - .map(paymentAccount -> (RevolutAccount) paymentAccount) - .filter(RevolutAccount::userNameNotSet) - .collect(Collectors.toList())); - } - } - @Nullable public Arbitrator getAcceptedArbitratorByAddress(NodeAddress nodeAddress) { final List acceptedArbitrators = userPayload.getAcceptedArbitrators(); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 94d4495f9f..874fdcc543 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -637,6 +637,7 @@ portfolio.pending.step2_buyer.bank=Please go to your online banking web page and # suppress inspection "TrailingSpacesInProperty" portfolio.pending.step2_buyer.f2f=Please contact the BTC seller by the provided contact and arrange a meeting to pay {0}.\n\n portfolio.pending.step2_buyer.startPaymentUsing=Start payment using {0} +portfolio.pending.step2_buyer.recipientsAccountData=Recipients {0} portfolio.pending.step2_buyer.amountToTransfer=Amount to transfer portfolio.pending.step2_buyer.sellersAddress=Seller''s {0} address portfolio.pending.step2_buyer.buyerAccount=Your payment account to be used @@ -755,7 +756,7 @@ portfolio.pending.step3_seller.buyersAddress=Buyers {0} address portfolio.pending.step3_seller.yourAccount=Your trading account portfolio.pending.step3_seller.xmrTxHash=Transaction ID portfolio.pending.step3_seller.xmrTxKey=Transaction key -portfolio.pending.step3_seller.buyersAccount=Buyers trading account +portfolio.pending.step3_seller.buyersAccount=Buyers account data portfolio.pending.step3_seller.confirmReceipt=Confirm payment receipt portfolio.pending.step3_seller.buyerStartedPayment=The BTC buyer has started the {0} payment.\n{1} portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Check for blockchain confirmations at your altcoin wallet or block explorer and confirm the payment when you have sufficient blockchain confirmations. @@ -1930,9 +1931,9 @@ dao.bond.bondedRoleType.DATA_RELAY_NODE_OPERATOR=Price node operator # suppress inspection "UnusedProperty" dao.bond.bondedRoleType.BTC_NODE_OPERATOR=Bitcoin node operator # suppress inspection "UnusedProperty" -dao.bond.bondedRoleType.MARKETS_OPERATOR=Markets API operator +dao.bond.bondedRoleType.MARKETS_OPERATOR=Markets operator # suppress inspection "UnusedProperty" -dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=BSQ explorer operator +dao.bond.bondedRoleType.BSQ_EXPLORER_OPERATOR=Explorer operator # suppress inspection "UnusedProperty" dao.bond.bondedRoleType.MOBILE_NOTIFICATIONS_RELAY_OPERATOR=Mobile notifications relay operator # suppress inspection "UnusedProperty" @@ -3044,6 +3045,7 @@ payment.account=Account payment.account.no=Account no. payment.account.name=Account name payment.account.userName=User name +payment.account.phoneNr=Phone number payment.account.owner=Account owner full name payment.account.fullName=Full name (first, middle, last) payment.account.state=State/Province/Region 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/components/paymentmethods/AssetsForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java index 125f685a2b..2933099ee1 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java @@ -38,6 +38,7 @@ import bisq.core.payment.validation.AltCoinAddressValidator; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; +import bisq.common.UserThread; import bisq.common.util.Tuple3; import org.apache.commons.lang3.StringUtils; @@ -123,6 +124,13 @@ public class AssetsForm extends PaymentMethodForm { addressInputTextField.setValidator(altCoinAddressValidator); addressInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + if (newValue.startsWith("monero:")) { + UserThread.execute(() -> { + String addressWithoutPrefix = newValue.replace("monero:", ""); + addressInputTextField.setText(addressWithoutPrefix); + }); + return; + } assetAccount.setAddress(newValue); updateFromInputs(); }); diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java index 6ad54d9433..8ed82f821e 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/RevolutForm.java @@ -32,15 +32,20 @@ import bisq.core.payment.payload.RevolutAccountPayload; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; +import bisq.common.util.Tuple2; + import javafx.scene.control.TextField; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; +import lombok.extern.slf4j.Slf4j; + import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static bisq.desktop.util.FormBuilder.addTopLabelFlowPane; import static bisq.desktop.util.FormBuilder.addTopLabelTextField; +@Slf4j public class RevolutForm extends PaymentMethodForm { private final RevolutAccount account; private RevolutValidator validator; @@ -48,9 +53,8 @@ public class RevolutForm extends PaymentMethodForm { public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) { - String userName = ((RevolutAccountPayload) paymentAccountPayload).getUserName(); - addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.userName"), userName); - + Tuple2 tuple = ((RevolutAccountPayload) paymentAccountPayload).getRecipientsAccountData(); + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, tuple.first, tuple.second); return gridRow; } @@ -104,9 +108,17 @@ public class RevolutForm extends PaymentMethodForm { account.getAccountName(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), Res.get(account.getPaymentMethod().getId())); + String userName = account.getUserName(); - TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.userName"), userName).second; - field.setMouseTransparent(false); + TextField userNameTf = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.userName"), userName).second; + userNameTf.setMouseTransparent(false); + + if (account.hasOldAccountId()) { + String accountId = account.getAccountId(); + TextField accountIdTf = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.phoneNr"), accountId).second; + accountIdTf.setMouseTransparent(false); + } + addLimitations(true); addCurrenciesGrid(false); } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java index 1f60ed26f6..8cee9fb458 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBook.java @@ -98,20 +98,24 @@ public class OfferBook { @Override public void onRemoved(Offer offer) { - // Update state in case that that offer is used in the take offer screen, so it gets updated correctly - offer.setState(Offer.State.REMOVED); - - // clean up possible references in openOfferManager - tradeManager.onOfferRemovedFromRemoteOfferBook(offer); - // We don't use the contains method as the equals method in Offer takes state and errorMessage into account. - Optional candidateToRemove = offerBookListItems.stream() - .filter(item -> item.getOffer().getId().equals(offer.getId())) - .findAny(); - candidateToRemove.ifPresent(offerBookListItems::remove); + removeOffer(offer, tradeManager); } }); } + public void removeOffer(Offer offer, TradeManager tradeManager) { + // Update state in case that that offer is used in the take offer screen, so it gets updated correctly + offer.setState(Offer.State.REMOVED); + + // clean up possible references in openOfferManager + tradeManager.onOfferRemovedFromRemoteOfferBook(offer); + // We don't use the contains method as the equals method in Offer takes state and errorMessage into account. + Optional candidateToRemove = offerBookListItems.stream() + .filter(item -> item.getOffer().getId().equals(offer.getId())) + .findAny(); + candidateToRemove.ifPresent(offerBookListItems::remove); + } + public ObservableList getOfferBookListItems() { return offerBookListItems; } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java index b35a9d6703..e34b3233a7 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -19,6 +19,7 @@ package bisq.desktop.main.offer.takeoffer; import bisq.desktop.Navigation; import bisq.desktop.main.offer.OfferDataModel; +import bisq.desktop.main.offer.offerbook.OfferBook; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.GUIUtil; @@ -81,6 +82,7 @@ import static com.google.common.base.Preconditions.checkNotNull; */ class TakeOfferDataModel extends OfferDataModel { private final TradeManager tradeManager; + private final OfferBook offerBook; private final BsqWalletService bsqWalletService; private final User user; private final FeeService feeService; @@ -120,6 +122,7 @@ class TakeOfferDataModel extends OfferDataModel { @Inject TakeOfferDataModel(TradeManager tradeManager, + OfferBook offerBook, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, User user, FeeService feeService, @@ -134,6 +137,7 @@ class TakeOfferDataModel extends OfferDataModel { super(btcWalletService); this.tradeManager = tradeManager; + this.offerBook = offerBook; this.bsqWalletService = bsqWalletService; this.user = user; this.feeService = feeService; @@ -291,6 +295,7 @@ class TakeOfferDataModel extends OfferDataModel { btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); } + /////////////////////////////////////////////////////////////////////////////////////////// // UI actions /////////////////////////////////////////////////////////////////////////////////////////// @@ -325,7 +330,17 @@ class TakeOfferDataModel extends OfferDataModel { offer, paymentAccount.getId(), useSavingsWallet, - tradeResultHandler, + trade -> { + // We do not wait until the offer got removed by a network remove message but remove it + // directly from the offer book. The broadcast gets now bundled and has 2 sec. delay so the + // removal from the network is a bit slower as it has been before. To avoid that the taker gets + // confused to see the same offer still in the offerbook we remove it manually. This removal has + // only local effect. Other trader might see the offer for a few seconds + // still (but cannot take it). + offerBook.removeOffer(checkNotNull(trade.getOffer()), tradeManager); + + tradeResultHandler.handleResult(trade); + }, errorMessage -> { log.warn(errorMessage); new Popup().warning(errorMessage).show(); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index c94317c17b..6524be8fc5 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -31,6 +31,8 @@ import bisq.desktop.util.Layout; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.PaymentAccountUtil; import bisq.core.payment.payload.AssetsAccountPayload; import bisq.core.payment.payload.BankAccountPayload; import bisq.core.payment.payload.CashDepositAccountPayload; @@ -74,17 +76,22 @@ import java.util.Optional; import javax.annotation.Nullable; import static bisq.desktop.util.FormBuilder.*; +import static com.google.common.base.Preconditions.checkNotNull; public class SellerStep3View extends TradeStepView { - private final ChangeListener proofResultListener; private Button confirmButton; private Label statusLabel; private BusyAnimation busyAnimation; private Subscription tradeStatePropertySubscription; private Timer timeoutTimer; + @Nullable private InfoTextField assetTxProofResultField; + @Nullable private TxConfidenceIndicator assetTxConfidenceIndicator; + @Nullable + private ChangeListener proofResultListener; + private boolean useXmrTxProof; /////////////////////////////////////////////////////////////////////////////////////////// @@ -93,10 +100,6 @@ public class SellerStep3View extends TradeStepView { public SellerStep3View(PendingTradesViewModel model) { super(model); - - proofResultListener = (observable, oldValue, newValue) -> { - applyAssetTxProofResult(trade.getAssetTxProofResult()); - }; } @Override @@ -157,15 +160,15 @@ public class SellerStep3View extends TradeStepView { } }); - // we listen for updates on the trade autoConfirmResult field - if (assetTxProofResultField != null) { + useXmrTxProof = getCurrencyCode(trade).equals("XMR"); + if (useXmrTxProof) { + proofResultListener = (observable, oldValue, newValue) -> { + applyAssetTxProofResult(trade.getAssetTxProofResult()); + }; trade.getAssetTxProofResultUpdateProperty().addListener(proofResultListener); + applyAssetTxProofResult(trade.getAssetTxProofResult()); } - - applyAssetTxProofResult(trade.getAssetTxProofResult()); - - confirmButton.setDisable(isDisputed()); } @Override @@ -183,11 +186,12 @@ public class SellerStep3View extends TradeStepView { timeoutTimer.stop(); } - if (assetTxProofResultField != null) { + if (useXmrTxProof) { trade.getAssetTxProofResultUpdateProperty().removeListener(proofResultListener); } } + /////////////////////////////////////////////////////////////////////////////////////////// // Content /////////////////////////////////////////////////////////////////////////////////////////// @@ -211,33 +215,44 @@ public class SellerStep3View extends TradeStepView { String myTitle = ""; String peersTitle = ""; boolean isBlockChain = false; - String nameByCode = CurrencyUtil.getNameByCode(trade.getOffer().getCurrencyCode()); + String nameByCode = CurrencyUtil.getNameByCode(getCurrencyCode(trade)); Contract contract = trade.getContract(); if (contract != null) { PaymentAccountPayload myPaymentAccountPayload = contract.getSellerPaymentAccountPayload(); PaymentAccountPayload peersPaymentAccountPayload = contract.getBuyerPaymentAccountPayload(); + + myPaymentDetails = PaymentAccountUtil.findPaymentAccount(myPaymentAccountPayload, model.getUser()) + .map(PaymentAccount::getAccountName) + .orElse(""); + if (myPaymentAccountPayload instanceof AssetsAccountPayload) { - myPaymentDetails = ((AssetsAccountPayload) myPaymentAccountPayload).getAddress(); + if (myPaymentDetails.isEmpty()) { + // Not expected + myPaymentDetails = ((AssetsAccountPayload) myPaymentAccountPayload).getAddress(); + } peersPaymentDetails = ((AssetsAccountPayload) peersPaymentAccountPayload).getAddress(); myTitle = Res.get("portfolio.pending.step3_seller.yourAddress", nameByCode); peersTitle = Res.get("portfolio.pending.step3_seller.buyersAddress", nameByCode); isBlockChain = true; } else { - myPaymentDetails = myPaymentAccountPayload.getPaymentDetails(); + if (myPaymentDetails.isEmpty()) { + // Not expected + myPaymentDetails = myPaymentAccountPayload.getPaymentDetails(); + } peersPaymentDetails = peersPaymentAccountPayload.getPaymentDetails(); myTitle = Res.get("portfolio.pending.step3_seller.yourAccount"); peersTitle = Res.get("portfolio.pending.step3_seller.buyersAccount"); } } - if (!isBlockChain && !trade.getOffer().getPaymentMethod().equals(PaymentMethod.F2F)) { + if (!isBlockChain && !checkNotNull(trade.getOffer()).getPaymentMethod().equals(PaymentMethod.F2F)) { addTopLabelTextFieldWithCopyIcon( gridPane, gridRow, 1, Res.get("shared.reasonForPayment"), model.dataModel.getReference(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE); GridPane.setRowSpan(titledGroupBg, 4); } - if (isBlockChain && trade.getOffer().getCurrencyCode().equals("XMR")) { + if (useXmrTxProof) { assetTxProofResultField = new InfoTextField(); Tuple2 topLabelWithVBox = getTopLabelWithVBox(Res.get("portfolio.pending.step3_seller.autoConf.status.label"), assetTxProofResultField); @@ -297,13 +312,19 @@ public class SellerStep3View extends TradeStepView { statusLabel = tuple.third; } + @Override + protected void deactivatePaymentButtons(boolean isDisabled) { + confirmButton.setDisable(isDisabled); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Info /////////////////////////////////////////////////////////////////////////////////////////// @Override protected String getInfoText() { - String currencyCode = model.dataModel.getCurrencyCode(); + String currencyCode = getCurrencyCode(trade); if (model.isBlockChainMethod()) { return Res.get("portfolio.pending.step3_seller.buyerStartedPayment", Res.get("portfolio.pending.step3_seller.buyerStartedPayment.altcoin", currencyCode)); } else { @@ -318,7 +339,7 @@ public class SellerStep3View extends TradeStepView { @Override protected String getFirstHalfOverWarnText() { String substitute = model.isBlockChainMethod() ? - Res.get("portfolio.pending.step3_seller.warn.part1a", model.dataModel.getCurrencyCode()) : + Res.get("portfolio.pending.step3_seller.warn.part1a", getCurrencyCode(trade)) : Res.get("portfolio.pending.step3_seller.warn.part1b"); return Res.get("portfolio.pending.step3_seller.warn.part2", substitute); @@ -349,39 +370,38 @@ public class SellerStep3View extends TradeStepView { // The confirmPaymentReceived call will trigger the trade protocol to do the payout tx. We want to be sure that we // are well connected to the Bitcoin network before triggering the broadcast. - if (!model.dataModel.isReadyForTxBroadcast()) { - return; - } - String key = "confirmPaymentReceived"; - if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { - PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); - String message = Res.get("portfolio.pending.step3_seller.onPaymentReceived.part1", CurrencyUtil.getNameByCode(model.dataModel.getCurrencyCode())); - if (!(paymentAccountPayload instanceof AssetsAccountPayload)) { - if (!(paymentAccountPayload instanceof WesternUnionAccountPayload) && - !(paymentAccountPayload instanceof HalCashAccountPayload) && - !(paymentAccountPayload instanceof F2FAccountPayload)) { - message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.fiat", trade.getShortId()); - } + if (model.dataModel.isReadyForTxBroadcast()) { + String key = "confirmPaymentReceived"; + if (!DevEnv.isDevMode() && DontShowAgainLookup.showAgain(key)) { + PaymentAccountPayload paymentAccountPayload = model.dataModel.getSellersPaymentAccountPayload(); + String message = Res.get("portfolio.pending.step3_seller.onPaymentReceived.part1", CurrencyUtil.getNameByCode(getCurrencyCode(trade))); + if (!(paymentAccountPayload instanceof AssetsAccountPayload)) { + if (!(paymentAccountPayload instanceof WesternUnionAccountPayload) && + !(paymentAccountPayload instanceof HalCashAccountPayload) && + !(paymentAccountPayload instanceof F2FAccountPayload)) { + message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.fiat", trade.getShortId()); + } - Optional optionalHolderName = getOptionalHolderName(); - if (optionalHolderName.isPresent()) { - message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.name", optionalHolderName.get()); + Optional optionalHolderName = getOptionalHolderName(); + if (optionalHolderName.isPresent()) { + message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.name", optionalHolderName.get()); + } } + message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.note"); + if (model.dataModel.isSignWitnessTrade()) { + message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.signer"); + } + new Popup() + .headLine(Res.get("portfolio.pending.step3_seller.onPaymentReceived.confirm.headline")) + .confirmation(message) + .width(700) + .actionButtonText(Res.get("portfolio.pending.step3_seller.onPaymentReceived.confirm.yes")) + .onAction(this::confirmPaymentReceived) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } else { + confirmPaymentReceived(); } - message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.note"); - if (model.dataModel.isSignWitnessTrade()) { - message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.signer"); - } - new Popup() - .headLine(Res.get("portfolio.pending.step3_seller.onPaymentReceived.confirm.headline")) - .confirmation(message) - .width(700) - .actionButtonText(Res.get("portfolio.pending.step3_seller.onPaymentReceived.confirm.yes")) - .onAction(this::confirmPaymentReceived) - .closeButtonText(Res.get("shared.cancel")) - .show(); - } else { - confirmPaymentReceived(); } } @@ -390,12 +410,12 @@ public class SellerStep3View extends TradeStepView { String key = "confirmPayment" + trade.getId(); String message = ""; String tradeVolumeWithCode = DisplayUtils.formatVolumeWithCode(trade.getTradeVolume()); - String currencyName = CurrencyUtil.getNameByCode(trade.getOffer().getCurrencyCode()); + String currencyName = CurrencyUtil.getNameByCode(getCurrencyCode(trade)); String part1 = Res.get("portfolio.pending.step3_seller.part", currencyName); String id = trade.getShortId(); if (paymentAccountPayload instanceof AssetsAccountPayload) { String address = ((AssetsAccountPayload) paymentAccountPayload).getAddress(); - String explorerOrWalletString = trade.getOffer().getCurrencyCode().equals("XMR") ? + String explorerOrWalletString = getCurrencyCode(trade).equals("XMR") ? Res.get("portfolio.pending.step3_seller.altcoin.wallet", currencyName) : Res.get("portfolio.pending.step3_seller.altcoin.explorer", currencyName); message = Res.get("portfolio.pending.step3_seller.altcoin", part1, explorerOrWalletString, address, tradeVolumeWithCode, currencyName); @@ -475,8 +495,12 @@ public class SellerStep3View extends TradeStepView { } private void applyAssetTxProofResult(@Nullable AssetTxProofResult result) { + checkNotNull(assetTxProofResultField); + checkNotNull(assetTxConfidenceIndicator); + String txt = GUIUtil.getProofResultAsString(result); assetTxProofResultField.setText(txt); + if (result == null) { assetTxConfidenceIndicator.setProgress(0); return; @@ -514,8 +538,7 @@ public class SellerStep3View extends TradeStepView { return label; } - @Override - protected void updateConfirmButtonDisableState(boolean isDisabled) { - confirmButton.setDisable(isDisabled); + private String getCurrencyCode(Trade trade) { + return CurrencyUtil.getNameByCode(checkNotNull(trade.getOffer()).getCurrencyCode()); } } 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..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 @@ -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 { protected final DisputeManager> disputeManager; @@ -122,7 +123,7 @@ public abstract class DisputeView extends ActivatableView { private final AccountAgeWitnessService accountAgeWitnessService; private final boolean useDevPrivilegeKeys; - private TableView tableView; + protected TableView tableView; private SortedList sortedList; @Getter @@ -132,16 +133,15 @@ public abstract class DisputeView extends ActivatableView { private ChangeListener selectedDisputeClosedPropertyListener; private Subscription selectedDisputeSubscription; - private EventHandler keyEventEventHandler; - private Scene scene; protected FilteredList filteredList; protected InputTextField filterTextField; private ChangeListener filterTextFieldListener; - private HBox filterBox; protected AutoTooltipButton reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton; private Map> disputeChatMessagesListeners = new HashMap<>(); @Nullable private ListChangeListener disputesListener; // Only set in mediation cases + protected Label alertIconLabel; + protected TableColumn stateColumn; /////////////////////////////////////////////////////////////////////////////////////////// @@ -180,6 +180,14 @@ public 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); @@ -217,9 +225,16 @@ 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, 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 { 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 { 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 { 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 { } } - protected void handleKeyPressed(KeyEvent event) { - } - protected void reOpenDispute() { if (selectedDispute != null) { selectedDispute.setIsClosed(false); @@ -712,7 +716,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); @@ -743,7 +747,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)); @@ -1099,7 +1103,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 582c461b08..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,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> 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 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); + } }