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:
chimp1984 2020-09-06 23:13:43 -05:00
commit f3c96bbb62
No known key found for this signature in database
GPG Key ID: 9801B4EC591F90E3
32 changed files with 795 additions and 162 deletions

View File

@ -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;
}

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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 = "";

View File

@ -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 = "";

View File

@ -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 = "";

View File

@ -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;
}
}

View File

@ -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.

View File

@ -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();
}

View File

@ -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 = "";

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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

View File

@ -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 = "";

View File

@ -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 = "";

View File

@ -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.

View File

@ -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"));
}
}

View File

@ -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

View File

@ -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");
}

View File

@ -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;

View File

@ -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();

View File

@ -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

View File

@ -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;
}

View File

@ -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();
});

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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();

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}