mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 09:52:23 +01:00
Merge branch 'master_upstream' into deactive-confirm-buttons-once-mediation-started
# Conflicts: # desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java
This commit is contained in:
commit
f3c96bbb62
@ -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;
|
||||
}
|
||||
|
@ -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<PaymentAccount> 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) {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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 = "";
|
||||
|
||||
|
@ -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<String, String> tuple = getLabelValueTuple();
|
||||
return Res.get(paymentMethodId) + " - " + tuple.first + ": " + tuple.second;
|
||||
}
|
||||
|
||||
private Tuple2<String, String> 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<String, String> getRecipientsAccountData() {
|
||||
Tuple2<String, String> 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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -82,6 +82,8 @@ public class XmrTxProofService implements AssetTxProofService {
|
||||
private Map<String, ChangeListener<Trade.State>> tradeStateListenerMap = new HashMap<>();
|
||||
private ChangeListener<Number> btcPeersListener, btcBlockListener;
|
||||
private BootstrapListener bootstrapListener;
|
||||
private MonadicBinding<Boolean> p2pNetworkAndWalletReady;
|
||||
private ChangeListener<Boolean> 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<Boolean> 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");
|
||||
}
|
||||
|
@ -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<String> 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<String> 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;
|
||||
|
||||
|
@ -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<List<RevolutAccount>> 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<Arbitrator> acceptedArbitrators = userPayload.getAcceptedArbitrators();
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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<String, String> 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);
|
||||
}
|
||||
|
@ -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<OfferBookListItem> 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<OfferBookListItem> candidateToRemove = offerBookListItems.stream()
|
||||
.filter(item -> item.getOffer().getId().equals(offer.getId()))
|
||||
.findAny();
|
||||
candidateToRemove.ifPresent(offerBookListItems::remove);
|
||||
}
|
||||
|
||||
public ObservableList<OfferBookListItem> getOfferBookListItems() {
|
||||
return offerBookListItems;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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<Number> 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<Number> 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<Label, VBox> 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<String> optionalHolderName = getOptionalHolderName();
|
||||
if (optionalHolderName.isPresent()) {
|
||||
message += Res.get("portfolio.pending.step3_seller.onPaymentReceived.name", optionalHolderName.get());
|
||||
Optional<String> 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());
|
||||
}
|
||||
}
|
||||
|
@ -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…
Reference in New Issue
Block a user