diff --git a/core/src/main/java/bisq/core/payment/CryptoCurrencyAccount.java b/core/src/main/java/bisq/core/payment/CryptoCurrencyAccount.java index 5493592538..b04aad1f4e 100644 --- a/core/src/main/java/bisq/core/payment/CryptoCurrencyAccount.java +++ b/core/src/main/java/bisq/core/payment/CryptoCurrencyAccount.java @@ -47,4 +47,8 @@ public final class CryptoCurrencyAccount extends AssetAccount { public @NonNull List getSupportedCurrencies() { return SUPPORTED_CURRENCIES; } + + public PaymentAccountPayload getPaymentAccountPayload() { + return paymentAccountPayload; + } } diff --git a/core/src/main/java/bisq/core/payment/PaymentAccount.java b/core/src/main/java/bisq/core/payment/PaymentAccount.java index d48ccf3485..25414d1b28 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccount.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccount.java @@ -24,11 +24,14 @@ import bisq.core.proto.CoreProtoResolver; import bisq.common.proto.ProtoUtil; import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.CollectionUtils; import bisq.common.util.Utilities; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; import java.util.stream.Collectors; @@ -69,6 +72,11 @@ public abstract class PaymentAccount implements PersistablePayload { @Nullable protected TradeCurrency selectedTradeCurrency; + // Was added at v1.9.2 + @Setter + @Nullable + protected Map extraData; + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -100,6 +108,7 @@ public abstract class PaymentAccount implements PersistablePayload { .setAccountName(accountName) .addAllTradeCurrencies(ProtoUtil.collectionToProto(tradeCurrencies, protobuf.TradeCurrency.class)); Optional.ofNullable(selectedTradeCurrency).ifPresent(selectedTradeCurrency -> builder.setSelectedTradeCurrency((protobuf.TradeCurrency) selectedTradeCurrency.toProtoMessage())); + Optional.ofNullable(extraData).ifPresent(builder::putAllExtraData); return builder.build(); } @@ -130,6 +139,11 @@ public abstract class PaymentAccount implements PersistablePayload { if (proto.hasSelectedTradeCurrency()) account.setSelectedTradeCurrency(TradeCurrency.fromProto(proto.getSelectedTradeCurrency())); + if (CollectionUtils.isEmpty(proto.getExtraDataMap())) { + account.setExtraData(null); + } else { + account.setExtraData(new HashMap<>(proto.getExtraDataMap())); + } return account; } catch (RuntimeException e) { log.warn("Could not load account: {}, exception: {}", paymentMethodId, e.toString()); @@ -265,4 +279,11 @@ public abstract class PaymentAccount implements PersistablePayload { @NonNull public abstract List getSupportedCurrencies(); + + public Map getOrCreateExtraData() { + if (extraData == null) { + extraData = new HashMap<>(); + } + return extraData; + } } diff --git a/core/src/main/java/bisq/core/payment/XmrAccountDelegate.java b/core/src/main/java/bisq/core/payment/XmrAccountDelegate.java new file mode 100644 index 0000000000..c9b4251940 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/XmrAccountDelegate.java @@ -0,0 +1,156 @@ +package bisq.core.payment; + +import bisq.core.xmr.knaccc.monero.address.WalletAddress; + +import java.util.Map; + +import lombok.Getter; +import lombok.experimental.Delegate; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Delegate for AssetAccount with convenient methods for managing the map entries and creating subAccounts. + */ +@Slf4j +public class XmrAccountDelegate { + public static final String USE_XMR_SUB_ADDRESSES = "UseXMmrSubAddresses"; + private static final String KEY_MAIN_ADDRESS = "XmrMainAddress"; + private static final String KEY_PRIVATE_VIEW_KEY = "XmrPrivateViewKey"; + private static final String KEY_ACCOUNT_INDEX = "XmrAccountIndex"; + private static final String KEY_SUB_ADDRESS_INDEX = "XmrSubAddressIndex"; + private static final String KEY_SUB_ADDRESS = "XmrSubAddress"; + private static final String KEY_TRADE_ID = "TradeId"; + + public static boolean isUsingSubAddresses(PaymentAccount paymentAccount) { + return paymentAccount.extraData != null && + paymentAccount.extraData.getOrDefault(USE_XMR_SUB_ADDRESSES, "0").equals("1"); + } + + public static long getSubAddressIndexAsLong(PaymentAccount paymentAccount) { + checkNotNull(paymentAccount.extraData, "paymentAccount.extraData must not be null"); + // We let it throw in case the value is not a number + try { + return Long.parseLong(paymentAccount.extraData.get(KEY_SUB_ADDRESS_INDEX)); + } catch (Throwable t) { + log.error("Could not parse value " + paymentAccount.extraData.get(KEY_SUB_ADDRESS_INDEX + " to long value."), t); + throw new RuntimeException(t); + } + } + + @Getter + @Delegate + private final AssetAccount account; + + public XmrAccountDelegate(AssetAccount account) { + this.account = account; + } + + public void createAndSetNewSubAddress() { + long accountIndex = Long.parseLong(getAccountIndex()); + long subAddressIndex = Long.parseLong(getSubAddressIndex()); + // If both subAddressIndex and accountIndex would be 0 it would be the main address + // and the walletAddress.getSubaddressBase58 call would return an error. + checkArgument(subAddressIndex >= 0 && accountIndex >= 0 && (subAddressIndex + accountIndex > 0), + "accountIndex and/or subAddressIndex are invalid"); + String privateViewKey = getPrivateViewKey(); + String mainAddress = getMainAddress(); + if (mainAddress.isEmpty() || privateViewKey.isEmpty()) { + return; + } + try { + WalletAddress walletAddress = new WalletAddress(mainAddress); + long ts = System.currentTimeMillis(); + String subAddress = walletAddress.getSubaddressBase58(privateViewKey, accountIndex, subAddressIndex); + log.info("Created new subAddress {}. Took {} ms.", subAddress, System.currentTimeMillis() - ts); + setSubAddress(subAddress); + } catch (WalletAddress.InvalidWalletAddressException e) { + log.error("WalletAddress.getSubaddressBase58 failed", e); + throw new RuntimeException(e); + } + } + + public void reset() { + getMap().remove(USE_XMR_SUB_ADDRESSES); + getMap().remove(KEY_MAIN_ADDRESS); + getMap().remove(KEY_PRIVATE_VIEW_KEY); + getMap().remove(KEY_ACCOUNT_INDEX); + getMap().remove(KEY_SUB_ADDRESS_INDEX); + getMap().remove(KEY_SUB_ADDRESS); + getMap().remove(KEY_TRADE_ID); + + account.setAddress(""); + } + + public boolean isUsingSubAddresses() { + return XmrAccountDelegate.isUsingSubAddresses(account); + } + + public void setIsUsingSubAddresses(boolean value) { + getMap().put(USE_XMR_SUB_ADDRESSES, value ? "1" : "0"); + } + + public String getSubAddress() { + return getMap().getOrDefault(KEY_SUB_ADDRESS, ""); + } + + public void setSubAddress(String subAddress) { + getMap().put(KEY_SUB_ADDRESS, subAddress); + account.setAddress(subAddress); + } + + // Unique ID for subAccount used as key in our global subAccount map. + public String getSubAccountId() { + return getMainAddress() + getAccountIndex(); + } + + public String getMainAddress() { + return getMap().getOrDefault(KEY_MAIN_ADDRESS, ""); + } + + public void setMainAddress(String mainAddress) { + getMap().put(KEY_MAIN_ADDRESS, mainAddress); + } + + public String getPrivateViewKey() { + return getMap().getOrDefault(KEY_PRIVATE_VIEW_KEY, ""); + } + + public void setPrivateViewKey(String privateViewKey) { + getMap().put(KEY_PRIVATE_VIEW_KEY, privateViewKey); + } + + public String getAccountIndex() { + return getMap().getOrDefault(KEY_ACCOUNT_INDEX, ""); + } + + public void setAccountIndex(String newValue) { + getMap().put(KEY_ACCOUNT_INDEX, newValue); + } + + public String getSubAddressIndex() { + return getMap().getOrDefault(KEY_SUB_ADDRESS_INDEX, ""); + } + + public long getSubAddressIndexAsLong() { + return XmrAccountDelegate.getSubAddressIndexAsLong(account); + } + + public void setSubAddressIndex(String subAddressIndex) { + getMap().put(KEY_SUB_ADDRESS_INDEX, subAddressIndex); + } + + public String getTradeId() { + return getMap().getOrDefault(KEY_TRADE_ID, ""); + } + + public void setTradeId(String tradeId) { + getMap().put(KEY_TRADE_ID, tradeId); + } + + private Map getMap() { + return account.getOrCreateExtraData(); + } +} diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index acfef2fb26..21d1615110 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -27,6 +27,8 @@ import bisq.core.offer.OfferDirection; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; import bisq.core.offer.availability.OfferAvailabilityModel; +import bisq.core.payment.PaymentAccount; +import bisq.core.proto.persistable.CorePersistenceProtoResolver; import bisq.core.provider.price.PriceFeedService; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.mediation.mediator.MediatorManager; @@ -110,6 +112,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -163,6 +166,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi @Getter private final LongProperty numPendingTrades = new SimpleLongProperty(); private final ReferralIdService referralIdService; + private final CorePersistenceProtoResolver corePersistenceProtoResolver; private final DumpDelayedPayoutTx dumpDelayedPayoutTx; @Getter private final boolean allowFaultyDelayedTxs; @@ -191,6 +195,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi ClockWatcher clockWatcher, PersistenceManager> persistenceManager, ReferralIdService referralIdService, + CorePersistenceProtoResolver corePersistenceProtoResolver, DumpDelayedPayoutTx dumpDelayedPayoutTx, @Named(Config.ALLOW_FAULTY_DELAYED_TXS) boolean allowFaultyDelayedTxs) { this.user = user; @@ -210,6 +215,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi this.provider = provider; this.clockWatcher = clockWatcher; this.referralIdService = referralIdService; + this.corePersistenceProtoResolver = corePersistenceProtoResolver; this.dumpDelayedPayoutTx = dumpDelayedPayoutTx; this.allowFaultyDelayedTxs = allowFaultyDelayedTxs; this.persistenceManager = persistenceManager; @@ -931,4 +937,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi AddressEntry.Context.TRADE_PAYOUT); return true; } + + public PaymentAccount cloneAccount(PaymentAccount paymentAccount) { + return Objects.requireNonNull(PaymentAccount.fromProto(paymentAccount.toProtoMessage(), corePersistenceProtoResolver)); + } } diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/DisputeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/DisputeProtocol.java index ee549e1cde..651a8422a4 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/DisputeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/DisputeProtocol.java @@ -67,6 +67,13 @@ public class DisputeProtocol extends TradeProtocol { this.processModel = trade.getProcessModel(); } + @Override + protected void onInitialized() { + super.onInitialized(); + processModel.applyPaymentAccount(trade); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // TradeProtocol implementation /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsMakerProtocol.java index 97841a715c..1da30e3e0d 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsMakerProtocol.java @@ -33,6 +33,7 @@ import bisq.core.trade.protocol.bisq_v1.tasks.maker.MakerProcessesInputsForDepos import bisq.core.trade.protocol.bisq_v1.tasks.maker.MakerRemovesOpenOffer; import bisq.core.trade.protocol.bisq_v1.tasks.maker.MakerSetsLockTime; import bisq.core.trade.protocol.bisq_v1.tasks.maker.MakerVerifyTakerFeePayment; +import bisq.core.trade.protocol.bisq_v1.tasks.seller.MaybeCreateSubAccount; import bisq.core.trade.protocol.bisq_v1.tasks.seller.SellerCreatesDelayedPayoutTx; import bisq.core.trade.protocol.bisq_v1.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest; import bisq.core.trade.protocol.bisq_v1.tasks.seller.SellerSignsDelayedPayoutTx; @@ -72,6 +73,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc .with(message) .from(peer)) .setup(tasks( + MaybeCreateSubAccount.class, MakerProcessesInputsForDepositTxRequest.class, ApplyFilter.class, getVerifyPeersFeePaymentClass(), diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsTakerProtocol.java index 8044ebf1d4..fae836be76 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/SellerAsTakerProtocol.java @@ -27,6 +27,7 @@ import bisq.core.trade.protocol.bisq_v1.messages.DelayedPayoutTxSignatureRespons import bisq.core.trade.protocol.bisq_v1.messages.InputsForDepositTxResponse; import bisq.core.trade.protocol.bisq_v1.tasks.ApplyFilter; import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask; +import bisq.core.trade.protocol.bisq_v1.tasks.seller.MaybeCreateSubAccount; import bisq.core.trade.protocol.bisq_v1.tasks.seller.SellerCreatesDelayedPayoutTx; import bisq.core.trade.protocol.bisq_v1.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest; import bisq.core.trade.protocol.bisq_v1.tasks.seller.SellerSignsDelayedPayoutTx; @@ -72,6 +73,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc .with(TakerEvent.TAKE_OFFER) .from(trade.getTradingPeerNodeAddress())) .setup(tasks( + MaybeCreateSubAccount.class, ApplyFilter.class, getVerifyPeersFeePaymentClass(), CreateTakerFeeTx.class, diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/model/ProcessModel.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/model/ProcessModel.java index c321912485..17b7f20fa1 100644 --- a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/model/ProcessModel.java +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/model/ProcessModel.java @@ -118,7 +118,6 @@ public class ProcessModel implements ProtocolModel { // Persistable Immutable private final TradingPeer tradingPeer; private final String offerId; - private final String accountId; private final PubKeyRing pubKeyRing; // Persistable Mutable @@ -161,6 +160,15 @@ public class ProcessModel implements ProtocolModel { @Setter private long sellerPayoutAmountFromMediation; + // Was changed at v1.9.2 from immutable to mutable + @Setter + private String accountId; + + // Was added at v1.9.2 + @Setter + @Nullable + private PaymentAccount paymentAccount; + // We want to indicate the user the state of the message delivery of the // CounterCurrencyTransferStartedMessage. As well we do an automatic re-send in case it was not ACKed yet. @@ -180,14 +188,18 @@ public class ProcessModel implements ProtocolModel { this.tradingPeer = tradingPeer != null ? tradingPeer : new TradingPeer(); } - public void applyTransient(Provider provider, - TradeManager tradeManager, - Offer offer) { + public void applyTransient(Provider provider, TradeManager tradeManager, Offer offer) { this.offer = offer; this.provider = provider; this.tradeManager = tradeManager; } + public void applyPaymentAccount(Trade trade) { + paymentAccount = trade instanceof MakerTrade ? + getUser().getPaymentAccount(offer.getMakerPaymentAccountId()) : + getUser().getPaymentAccount(trade.getTakerPaymentAccountId()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // PROTO BUFFER @@ -216,6 +228,7 @@ public class ProcessModel implements ProtocolModel { Optional.ofNullable(myMultiSigPubKey).ifPresent(e -> builder.setMyMultiSigPubKey(ByteString.copyFrom(myMultiSigPubKey))); Optional.ofNullable(tempTradingPeerNodeAddress).ifPresent(e -> builder.setTempTradingPeerNodeAddress(tempTradingPeerNodeAddress.toProtoMessage())); Optional.ofNullable(mediatedPayoutTxSignature).ifPresent(e -> builder.setMediatedPayoutTxSignature(ByteString.copyFrom(e))); + Optional.ofNullable(paymentAccount).ifPresent(e -> builder.setPaymentAccount(e.toProtoMessage())); return builder.build(); } @@ -247,6 +260,9 @@ public class ProcessModel implements ProtocolModel { MessageState paymentStartedMessageState = ProtoUtil.enumFromProto(MessageState.class, paymentStartedMessageStateString); processModel.setPaymentStartedMessageState(paymentStartedMessageState); + if (proto.hasPaymentAccount()) { + processModel.setPaymentAccount(PaymentAccount.fromProto(proto.getPaymentAccount(), coreProtoResolver)); + } return processModel; } @@ -271,12 +287,13 @@ public class ProcessModel implements ProtocolModel { @Nullable public PaymentAccountPayload getPaymentAccountPayload(Trade trade) { - PaymentAccount paymentAccount; - if (trade instanceof MakerTrade) - paymentAccount = getUser().getPaymentAccount(offer.getMakerPaymentAccountId()); - else - paymentAccount = getUser().getPaymentAccount(trade.getTakerPaymentAccountId()); - return paymentAccount != null ? paymentAccount.getPaymentAccountPayload() : null; + if (paymentAccount == null) { + // Persisted trades pre v 1.9.2 have no paymentAccount set, so it will be null. + // We do not need to persist it (though it would not hurt as well). + applyPaymentAccount(trade); + } + + return paymentAccount.getPaymentAccountPayload(); } public Coin getFundsNeededForTrade() { diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/seller/MaybeCreateSubAccount.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/seller/MaybeCreateSubAccount.java new file mode 100644 index 0000000000..fccbe8592d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v1/tasks/seller/MaybeCreateSubAccount.java @@ -0,0 +1,103 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.core.trade.protocol.bisq_v1.tasks.seller; + +import bisq.core.payment.AssetAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.XmrAccountDelegate; +import bisq.core.trade.model.bisq_v1.Trade; +import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class MaybeCreateSubAccount extends TradeTask { + + public MaybeCreateSubAccount(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + PaymentAccount parentAccount = Objects.requireNonNull(processModel.getPaymentAccount()); + + // This is a seller task, so no need to check for it + if (!trade.getOffer().isXmr() || + parentAccount.getExtraData() == null || + parentAccount.getExtraData().isEmpty() || + !XmrAccountDelegate.isUsingSubAddresses(parentAccount)) { + complete(); + return; + } + + // In case we are a seller using XMR sub addresses we clone the account, add it as xmrAccount and + // increment from the highest subAddressIndex from all our subAccounts grouped by the subAccountId (mainAddress + accountIndex). + PaymentAccount paymentAccount = processModel.getTradeManager().cloneAccount(Objects.requireNonNull(parentAccount)); + XmrAccountDelegate xmrAccountDelegate = new XmrAccountDelegate((AssetAccount) paymentAccount); + // We overwrite some fields + xmrAccountDelegate.setId(UUID.randomUUID().toString()); + xmrAccountDelegate.setTradeId(trade.getId()); + xmrAccountDelegate.setCreationDate(new Date().getTime()); + // We add our cloned account as xmrAccount and apply the incremented index and subAddress. + + // We need to store that globally, so we use the user object. + Map> subAccountsBySubAccountId = processModel.getUser().getSubAccountsById(); + subAccountsBySubAccountId.putIfAbsent(xmrAccountDelegate.getSubAccountId(), new HashSet<>()); + Set subAccounts = subAccountsBySubAccountId.get(xmrAccountDelegate.getSubAccountId()); + + // At first subAccount we use the index of the parent account and decrement by 1 as we will increment later in the code + long initialSubAccountIndex = xmrAccountDelegate.getSubAddressIndexAsLong() - 1; + long maxSubAddressIndex = subAccounts.stream() + .mapToLong(XmrAccountDelegate::getSubAddressIndexAsLong) + .max() + .orElse(initialSubAccountIndex); + + // Always increment, use the (decremented) initialSubAccountIndex or the next after max + ++maxSubAddressIndex; + + // Prefix subAddressIndex to account name + xmrAccountDelegate.setAccountName("[" + maxSubAddressIndex + "] " + parentAccount.getAccountName()); + xmrAccountDelegate.setSubAddressIndex(String.valueOf(maxSubAddressIndex)); + xmrAccountDelegate.createAndSetNewSubAddress(); + subAccounts.add(xmrAccountDelegate.getAccount()); + + // Now we set our xmrAccount as paymentAccount + processModel.setPaymentAccount(xmrAccountDelegate.getAccount()); + // We got set the accountId from the parent account at the ProcessModel constructor. We update it to the subAccounts id. + processModel.setAccountId(xmrAccountDelegate.getId()); + processModel.getUser().requestPersistence(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} + diff --git a/core/src/main/java/bisq/core/user/User.java b/core/src/main/java/bisq/core/user/User.java index 22555eef91..619d4e607a 100644 --- a/core/src/main/java/bisq/core/user/User.java +++ b/core/src/main/java/bisq/core/user/User.java @@ -51,6 +51,7 @@ import javafx.collections.SetChangeListener; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -538,4 +539,8 @@ public class User implements PersistedDataHost { public Cookie getCookie() { return userPayload.getCookie(); } + + public Map> getSubAccountsById() { + return userPayload.getSubAccountsById(); + } } diff --git a/core/src/main/java/bisq/core/user/UserPayload.java b/core/src/main/java/bisq/core/user/UserPayload.java index 1946f945a7..a1f4b2e0a0 100644 --- a/core/src/main/java/bisq/core/user/UserPayload.java +++ b/core/src/main/java/bisq/core/user/UserPayload.java @@ -31,14 +31,15 @@ import bisq.common.proto.ProtoUtil; import bisq.common.proto.persistable.PersistableEnvelope; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -46,7 +47,6 @@ import javax.annotation.Nullable; @Slf4j @Data -@AllArgsConstructor public class UserPayload implements PersistableEnvelope { @Nullable private String accountId; @@ -81,14 +81,55 @@ public class UserPayload implements PersistableEnvelope { @Nullable private List acceptedRefundAgents = new ArrayList<>(); - // Added at 1.5.3 + // Added at v1.5.3 // Generic map for persisting various UI states. We keep values un-typed as string to // provide sufficient flexibility. private Cookie cookie = new Cookie(); + // Was added at v1.9.2 + // Key is in case of XMR subAccounts the subAccountId (mainAddress + accountIndex). This creates unique sets of + // mainAddress + accountIndex combinations. + private Map> subAccountsById = new HashMap<>(); + public UserPayload() { } + public UserPayload(String accountId, + Set paymentAccounts, + PaymentAccount currentPaymentAccount, + List acceptedLanguageLocaleCodes, + Alert developersAlert, + Alert displayedAlert, + Filter developersFilter, + Arbitrator registeredArbitrator, + Mediator registeredMediator, + List acceptedArbitrators, + List acceptedMediators, + PriceAlertFilter priceAlertFilter, + List marketAlertFilters, + RefundAgent registeredRefundAgent, + List acceptedRefundAgents, + Cookie cookie, + Map> subAccountsById) { + this.accountId = accountId; + this.paymentAccounts = paymentAccounts; + this.currentPaymentAccount = currentPaymentAccount; + this.acceptedLanguageLocaleCodes = acceptedLanguageLocaleCodes; + this.developersAlert = developersAlert; + this.displayedAlert = displayedAlert; + this.developersFilter = developersFilter; + this.registeredArbitrator = registeredArbitrator; + this.registeredMediator = registeredMediator; + this.acceptedArbitrators = acceptedArbitrators; + this.acceptedMediators = acceptedMediators; + this.priceAlertFilter = priceAlertFilter; + this.marketAlertFilters = marketAlertFilters; + this.registeredRefundAgent = registeredRefundAgent; + this.acceptedRefundAgents = acceptedRefundAgents; + this.cookie = cookie; + this.subAccountsById = subAccountsById; + } + @Override public protobuf.PersistableEnvelope toProtoMessage() { protobuf.UserPayload.Builder builder = protobuf.UserPayload.newBuilder(); @@ -125,10 +166,27 @@ public class UserPayload implements PersistableEnvelope { .ifPresent(e -> builder.addAllAcceptedRefundAgents(ProtoUtil.collectionToProto(acceptedRefundAgents, message -> ((protobuf.StoragePayload) message).getRefundAgent()))); Optional.ofNullable(cookie).ifPresent(e -> builder.putAllCookie(cookie.toProtoMessage())); + + // We transform our map to a list of SubAccountEntries because protobuf has no good support for maps + builder.addAllSubAccountMapEntries(subAccountsById.entrySet().stream() + .map(mapEntry -> protobuf.SubAccountMapEntry.newBuilder() + .setKey(mapEntry.getKey()) + .addAllValue(mapEntry.getValue().stream() + .map(PaymentAccount::toProtoMessage) + .collect(Collectors.toList())) + .build()) + .collect(Collectors.toList())); return protobuf.PersistableEnvelope.newBuilder().setUserPayload(builder).build(); } public static UserPayload fromProto(protobuf.UserPayload proto, CoreProtoResolver coreProtoResolver) { + // We map the protobuf list to our map (due weak protobuf support for maps) + Map> subAccounts = proto.getSubAccountMapEntriesList().stream() + .collect(Collectors.toMap(protobuf.SubAccountMapEntry::getKey, + subAccountMapEntry -> subAccountMapEntry.getValueList().stream() + .map(subAccount -> PaymentAccount.fromProto(subAccount, coreProtoResolver)) + .collect(Collectors.toSet()))); + return new UserPayload( ProtoUtil.stringOrNullFromProto(proto.getAccountId()), proto.getPaymentAccountsList().isEmpty() ? new HashSet<>() : new HashSet<>(proto.getPaymentAccountsList().stream() @@ -156,7 +214,8 @@ public class UserPayload implements PersistableEnvelope { proto.getAcceptedRefundAgentsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedRefundAgentsList().stream() .map(RefundAgent::fromProto) .collect(Collectors.toList())), - Cookie.fromProto(proto.getCookieMap()) + Cookie.fromProto(proto.getCookieMap()), + subAccounts ); } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 50eecbee7d..990b9ef944 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1648,10 +1648,17 @@ but you need to enable it in Settings.\n\n\ See the wiki for more information about the auto-confirm feature: [HYPERLINK:https://bisq.wiki/Trading_Monero#Auto-confirming_trades].\n\n\ Please be aware that when changing the Bisq data directory you must change the Monero wallet and address to avoid \ vulnerabilities related to transaction key re-use. More details can be found here: [HYPERLINK:https://www.getmonero.org/2018/09/25/a-post-mortum-of-the-burning-bug.html]. - -account.altcoin.popup.xmr.dataDirWarning=Please be aware that when changing the Bisq data directory you must change the Monero wallet and address to avoid \ - vulnerabilities related to transaction key re-use. More details can be found here: [HYPERLINK:https://www.getmonero.org/2018/09/25/a-post-mortum-of-the-burning-bug.html]. - +account.altcoin.popup.xmr.dataDirWarningHeadline=Security recommendation for Monero traders +account.altcoin.popup.xmr.dataDirWarning=With v1.9.2 subaddress [HYPERLINK:https://monerodocs.org/public-address/subaddress] support has been added to Bisq.\n\n\ + It is recommended to create a new XMR account using subaddresses. This creates a new subaddress at each trade.\n\n\ + If changing the Bisq data directory you need to ensure to not re-use the address due known vulnerabilities [HYPERLINK:https://www.getmonero.org/2018/09/25/a-post-mortum-of-the-burning-bug.html].\n\ + Best way to achieve that, is by using a different Monero wallet for each Bisq instance. Otherwise, make sure to increment \ + the highest ever used subaddress index or use a new mainaddress or account index. +account.altcoin.popup.xmr.subAddressHeadline=Monero Subaddresses +account.altcoin.popup.xmr.subAddressInfo=Information about monero subaddresses can be found here: \ + [HYPERLINK:https://monerodocs.org/public-address/subaddress]\n\n\ + Your main XMR wallet address and View Key [HYPERLINK:https://www.getmonero.org/resources/user-guides/view_only.html] \ + are needed for Bisq to derive a new subaddress per trade. # suppress inspection "UnusedProperty" account.altcoin.popup.msr.msg=Trading MSR on Bisq requires that you understand and fulfill \ the following requirements:\n\n\ @@ -3442,6 +3449,16 @@ payment.altcoin.tradeInstant.popup=For instant trading it is required that both to complete the trade in less than 1 hour.\n\n\ If you have offers open and you are not available please disable \ those offers under the 'Portfolio' screen. +payment.altcoin.useSubAddresses=Use subaddresses +payment.altcoin.xmrSubAddress=Subaddress +payment.altcoin.xmrAccountIndex=Account Index +payment.altcoin.initialXmrSubAddressIndex=Initial Subaddress Index +payment.altcoin.xmrSubAddressesUsed=Subaddresses used in trades +payment.altcoin.xmrMainAddress=Main address +payment.altcoin.privateViewKey=Private view key + +payment.altcoin.usedSubaddressList=Index: {0} Subaddress: {1} Used in trade: {2} + payment.altcoin=Altcoin payment.select.altcoin=Select or search Altcoin payment.select.altcoin.bsq.warning=You can also trade BSQ with the new BSQ Swap protocol.\n\n\ @@ -4298,9 +4315,6 @@ news.bsqSwap.description=BSQ swaps is a new trade protocol for atomically swappi mediation or arbitration support. No account setup is required either.\n\n\ See more about BSQ swaps in documentation [HYPERLINK:https://bisq.wiki/BSQ_swaps]. news.mediationRules.title=Rules for Successful Trading -news.mediationRules.info=We'd like to ask you to make yourself familiar with Bisq's trading \ - rules [HYPERLINK:https://bisq.wiki/Trading_rules], and the penalties [HYPERLINK:https://bisq.wiki/Table_of_penalties] \ - for breaking them. Please check the following linked resources.\n\n\ - "KNOW THE PENALTIES TO AVOID THE PENALTIES"\n\n\ - Sincerely,\n\ - The Bisq Support Team -- on Matrix: [HYPERLINK:https://bisq.chat]. +news.mediationRules.info=Please make yourself familiar with Bisq's trading \ + rules [HYPERLINK:https://bisq.wiki/Trading_rules], [HYPERLINK:https://bisq.wiki/Table_of_penalties].\n\ + If you have questions feel free to get in touch with the Bisq Support team [HYPERLINK:https://bisq.chat]. diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java index 8c30cd1ea1..8c089bc5e9 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java @@ -17,16 +17,12 @@ package bisq.desktop.components.paymentmethods; -import bisq.desktop.components.AutocompleteComboBox; import bisq.desktop.components.InputTextField; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.FormBuilder; -import bisq.desktop.util.Layout; import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.dao.governance.asset.AssetService; -import bisq.core.filter.FilterManager; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.payment.AssetAccount; @@ -49,24 +45,18 @@ import javafx.scene.layout.VBox; import javafx.geometry.Insets; -import javafx.util.StringConverter; - import static bisq.desktop.util.DisplayUtils.createAssetsAccountName; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; import static bisq.desktop.util.FormBuilder.addLabelCheckBox; -import static bisq.desktop.util.GUIUtil.getComboBoxButtonCell; public class AssetsForm extends PaymentMethodForm { - public static final String INSTANT_TRADE_NEWS = "instantTradeNews0.9.5"; - private final AssetAccount assetAccount; - private final AltCoinAddressValidator altCoinAddressValidator; - private final AssetService assetService; - private final FilterManager filterManager; - - private InputTextField addressInputTextField; - private CheckBox tradeInstantCheckBox; - private boolean tradeInstant; + protected final AssetAccount assetAccount; + protected final AltCoinAddressValidator altCoinAddressValidator; + protected final AssetService assetService; + protected InputTextField addressInputTextField; + protected CheckBox tradeInstantCheckBox; + protected boolean tradeInstant; public static int addFormForBuyer(GridPane gridPane, int gridRow, @@ -84,13 +74,11 @@ public class AssetsForm extends PaymentMethodForm { GridPane gridPane, int gridRow, CoinFormatter formatter, - AssetService assetService, - FilterManager filterManager) { + AssetService assetService) { super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter); this.assetAccount = (AssetAccount) paymentAccount; this.altCoinAddressValidator = altCoinAddressValidator; this.assetService = assetService; - this.filterManager = filterManager; tradeInstant = paymentAccount instanceof InstantCryptoCurrencyAccount; } @@ -99,9 +87,6 @@ public class AssetsForm extends PaymentMethodForm { public void addFormForAddAccount() { gridRowFrom = gridRow + 1; - addTradeCurrencyComboBox(); - currencyComboBox.setPrefWidth(250); - tradeInstantCheckBox = addLabelCheckBox(gridPane, ++gridRow, Res.get("payment.altcoin.tradeInstantCheckbox"), 10); tradeInstantCheckBox.setSelected(tradeInstant); @@ -114,7 +99,6 @@ public class AssetsForm extends PaymentMethodForm { gridPane.getChildren().remove(tradeInstantCheckBox); tradeInstantCheckBox.setPadding(new Insets(0, 40, 0, 0)); - gridPane.getChildren().add(tradeInstantCheckBox); addressInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, @@ -196,45 +180,4 @@ public class AssetsForm extends PaymentMethodForm { && assetAccount.getSingleTradeCurrency() != null); } } - - @Override - protected void addTradeCurrencyComboBox() { - currencyComboBox = FormBuilder.addLabelAutocompleteComboBox(gridPane, ++gridRow, Res.get("payment.altcoin"), - Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; - currencyComboBox.setPromptText(Res.get("payment.select.altcoin")); - currencyComboBox.setButtonCell(getComboBoxButtonCell(Res.get("payment.select.altcoin"), currencyComboBox)); - - currencyComboBox.getEditor().focusedProperty().addListener(observable -> - currencyComboBox.setPromptText("")); - - ((AutocompleteComboBox) currencyComboBox).setAutocompleteItems( - CurrencyUtil.getActiveSortedCryptoCurrencies(assetService, filterManager)); - currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 10)); - - currencyComboBox.setConverter(new StringConverter<>() { - @Override - public String toString(TradeCurrency tradeCurrency) { - return tradeCurrency != null ? tradeCurrency.getNameAndCode() : ""; - } - - @Override - public TradeCurrency fromString(String s) { - return currencyComboBox.getItems().stream(). - filter(item -> item.getNameAndCode().equals(s)). - findAny().orElse(null); - } - }); - - ((AutocompleteComboBox) currencyComboBox).setOnChangeConfirmed(e -> { - addressInputTextField.resetValidation(); - addressInputTextField.validate(); - TradeCurrency tradeCurrency = currencyComboBox.getSelectionModel().getSelectedItem(); - paymentAccount.setSingleTradeCurrency(tradeCurrency); - updateFromInputs(); - - if (tradeCurrency != null && tradeCurrency.getCode().equals("BSQ")) { - new Popup().information(Res.get("payment.select.altcoin.bsq.warning")).show(); - } - }); - } } diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/XmrForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/XmrForm.java new file mode 100644 index 0000000000..7e3517d646 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/XmrForm.java @@ -0,0 +1,382 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.components.paymentmethods; + +import bisq.desktop.components.BisqTextArea; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.Layout; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.dao.governance.asset.AssetService; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.payment.AssetAccount; +import bisq.core.payment.InstantCryptoCurrencyAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.XmrAccountDelegate; +import bisq.core.payment.payload.AssetsAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.validation.AltCoinAddressValidator; +import bisq.core.user.DontShowAgainLookup; +import bisq.core.user.User; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.InputValidator; +import bisq.core.util.validation.IntegerValidator; +import bisq.core.util.validation.RegexValidator; + +import bisq.asset.AddressValidationResult; +import bisq.asset.CryptoNoteAddressValidator; + +import bisq.common.util.Tuple3; +import bisq.common.util.Utilities; + +import javafx.scene.control.CheckBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.control.TitledPane; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.core.xmr.knaccc.monero.address.WalletAddress.PUBLIC_ADDRESS_PREFIX; +import static bisq.desktop.util.DisplayUtils.createAssetsAccountName; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField; +import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon; +import static bisq.desktop.util.FormBuilder.addLabelCheckBox; + +@Slf4j +public class XmrForm extends AssetsForm { + private InputTextField privateViewKeyInputTextField, accountIndex, subAddressIndex, mainAddressTextField, subAddressTextField; + private CheckBox useSubAddressesCheckBox; + private boolean disableUpdates = false; + private final XmrWalletAddressValidator mainAddressValidator = new XmrWalletAddressValidator(); + private final IntegerValidator accountIndexValidator = new IntegerValidator(0, 99); + private final IntegerValidator subAddressIndexValidator = new IntegerValidator(0, 9999); + private final RegexValidator regexValidator = new RegexValidator(); + private final XmrAccountDelegate xmrAccountDelegate; + private final User user; + + public static int addFormForBuyer(GridPane gridPane, + int gridRow, + PaymentAccountPayload paymentAccountPayload, + String labelTitle) { + addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, labelTitle, + ((AssetsAccountPayload) paymentAccountPayload).getAddress()); + return gridRow; + } + + public XmrForm(PaymentAccount paymentAccount, + AccountAgeWitnessService accountAgeWitnessService, + AltCoinAddressValidator altCoinAddressValidator, + InputValidator inputValidator, + GridPane gridPane, + int gridRow, + CoinFormatter formatter, + AssetService assetService, + User user) { + super(paymentAccount, accountAgeWitnessService, altCoinAddressValidator, inputValidator, gridPane, gridRow, formatter, assetService); + + this.user = user; + xmrAccountDelegate = new XmrAccountDelegate(assetAccount); + } + + @Override + public void addFormForAddAccount() { + gridRowFrom = gridRow + 1; + + tradeInstantCheckBox = addLabelCheckBox(gridPane, ++gridRow, Res.get("payment.altcoin.tradeInstantCheckbox"), 10); + tradeInstantCheckBox.setSelected(tradeInstant); + tradeInstantCheckBox.setOnAction(e -> { + tradeInstant = tradeInstantCheckBox.isSelected(); + if (tradeInstant) + new Popup().information(Res.get("payment.altcoin.tradeInstant.popup")).show(); + paymentLimitationsTextField.setText(getLimitationsText()); + }); + tradeInstantCheckBox.setPadding(new Insets(0, 40, 0, 0)); + + useSubAddressesCheckBox = addLabelCheckBox(gridPane, ++gridRow, Res.get("payment.altcoin.useSubAddresses"), 10); + useSubAddressesCheckBox.setPadding(new Insets(0, 40, 0, 0)); + useSubAddressesCheckBox.setSelected(xmrAccountDelegate.isUsingSubAddresses()); + useSubAddressesCheckBox.setOnAction(e -> { + disableUpdates = true; + xmrAccountDelegate.reset(); + xmrAccountDelegate.setIsUsingSubAddresses(useSubAddressesCheckBox.isSelected()); + if (useSubAddressesCheckBox.isSelected()) { + xmrAccountDelegate.setAccountIndex("0"); + xmrAccountDelegate.setSubAddressIndex("1"); + maybeShowXmrSubAddressInfo(); + } + setFieldManagement(xmrAccountDelegate.isUsingSubAddresses()); + mainAddressTextField.setText(xmrAccountDelegate.getMainAddress()); + privateViewKeyInputTextField.setText(xmrAccountDelegate.getPrivateViewKey()); + accountIndex.setText(xmrAccountDelegate.getAccountIndex()); + subAddressIndex.setText(xmrAccountDelegate.getSubAddressIndex()); + subAddressTextField.setText(xmrAccountDelegate.getSubAddress()); + addressInputTextField.setText(xmrAccountDelegate.getSubAddress()); + disableUpdates = false; + updateFromInputs(); + }); + + mainAddressTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.altcoin.xmrMainAddress")); + mainAddressTextField.setValidator(mainAddressValidator); + mainAddressTextField.textProperty().addListener((ov, oldValue, newValue) -> { + updateFromInputs(); + }); + + privateViewKeyInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.altcoin.privateViewKey")); + regexValidator.setPattern("[a-fA-F0-9]{64}|^$"); + regexValidator.setErrorMessage(Res.get("portfolio.pending.step2_buyer.confirmStart.proof.invalidInput")); + privateViewKeyInputTextField.setValidator(regexValidator); + privateViewKeyInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + updateFromInputs(); + }); + + HBox hBox = new HBox(); + hBox.setSpacing(10); + + accountIndex = new InputTextField(); + accountIndex.setLabelFloat(true); + accountIndex.setPromptText(Res.get("payment.altcoin.xmrAccountIndex")); + accountIndex.setPrefWidth(100); + accountIndex.setValidator(accountIndexValidator); + accountIndex.textProperty().addListener((ov, oldValue, newValue) -> { + updateFromInputs(); + }); + + subAddressIndex = new InputTextField(); + subAddressIndex.setLabelFloat(true); + subAddressIndex.setPromptText(Res.get("payment.altcoin.initialXmrSubAddressIndex")); + subAddressIndex.setPrefWidth(130); + subAddressIndex.setValidator(subAddressIndexValidator); + subAddressIndex.textProperty().addListener((ov, oldValue, newValue) -> { + updateFromInputs(); + }); + + subAddressTextField = new InputTextField(); + subAddressTextField.setLabelFloat(true); + subAddressTextField.setPromptText(Res.get("payment.altcoin.xmrSubAddress")); + subAddressTextField.setDisable(true); // this field gets calculated, so read-only + subAddressTextField.setPrefWidth(750); + hBox.getChildren().addAll(accountIndex, subAddressIndex, subAddressTextField); + GridPane.setRowIndex(hBox, ++gridRow); + GridPane.setColumnIndex(hBox, 0); + GridPane.setMargin(hBox, new Insets(0 + Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); + gridPane.getChildren().add(hBox); + + // subAddressTextField and addressInputTextField share the same row, they are used interchangably + // depending on if subaddresses are in use + addressInputTextField = FormBuilder.addInputTextField(gridPane, gridRow, Res.get("payment.altcoin.address")); + addressInputTextField.setValidator(altCoinAddressValidator); + addressInputTextField.textProperty().addListener((ov, oldValue, newValue) -> { + updateFromInputs(); + }); + + setFieldManagement(xmrAccountDelegate.isUsingSubAddresses()); + addLimitations(false); + addAccountNameTextFieldWithAutoFillToggleButton(); + } + + void setFieldManagement(boolean useSubAddresses) { + useSubAddressesCheckBox.setManaged(true); + useSubAddressesCheckBox.setVisible(true); + mainAddressTextField.setManaged(useSubAddresses); + mainAddressTextField.setVisible(useSubAddresses); + privateViewKeyInputTextField.setManaged(useSubAddresses); + privateViewKeyInputTextField.setVisible(useSubAddresses); + accountIndex.setManaged(useSubAddresses); + accountIndex.setVisible(useSubAddresses); + subAddressIndex.setManaged(useSubAddresses); + subAddressIndex.setVisible(useSubAddresses); + subAddressTextField.setManaged(useSubAddresses); + subAddressTextField.setVisible(useSubAddresses); + addressInputTextField.setManaged(!useSubAddresses); + addressInputTextField.setVisible(!useSubAddresses); + } + + @Override + public PaymentAccount getPaymentAccount() { + if (tradeInstant) { + InstantCryptoCurrencyAccount instantCryptoCurrencyAccount = new InstantCryptoCurrencyAccount(); + instantCryptoCurrencyAccount.init(); + instantCryptoCurrencyAccount.setAccountName(paymentAccount.getAccountName()); + instantCryptoCurrencyAccount.setSaltAsHex(paymentAccount.getSaltAsHex()); + instantCryptoCurrencyAccount.setSalt(paymentAccount.getSalt()); + instantCryptoCurrencyAccount.setSingleTradeCurrency(paymentAccount.getSingleTradeCurrency()); + instantCryptoCurrencyAccount.setSelectedTradeCurrency(paymentAccount.getSelectedTradeCurrency()); + instantCryptoCurrencyAccount.setAddress(xmrAccountDelegate.getAddress()); + instantCryptoCurrencyAccount.setExtraData(paymentAccount.getExtraData()); + return instantCryptoCurrencyAccount; + } else { + return paymentAccount; + } + } + + @Override + public void updateFromInputs() { + if (disableUpdates) { + return; + } + disableUpdates = true; + if (xmrAccountDelegate.isUsingSubAddresses()) { + xmrAccountDelegate.setMainAddress(mainAddressTextField.getText()); + xmrAccountDelegate.setPrivateViewKey(privateViewKeyInputTextField.getText()); + xmrAccountDelegate.setAccountIndex(accountIndex.getText()); + xmrAccountDelegate.setSubAddressIndex(subAddressIndex.getText()); + if (accountIndex.validate() && subAddressIndex.validate() + && mainAddressTextField.validate() + && privateViewKeyInputTextField.validate() + && mainAddressTextField.getText().length() > 0 + && privateViewKeyInputTextField.getText().length() > 0) { + try { + xmrAccountDelegate.createAndSetNewSubAddress(); + } catch (Exception ex) { + log.warn(ex.toString()); + } + subAddressTextField.setText(xmrAccountDelegate.getSubAddress()); + } else { + xmrAccountDelegate.setSubAddress(""); + subAddressTextField.setText(""); + } + } else { + // legacy XMR (no subAddress) + xmrAccountDelegate.setAddress(addressInputTextField.getText()); + } + super.updateFromInputs(); + disableUpdates = false; + } + + @Override + protected void autoFillNameTextField() { + if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) { + accountNameTextField.setText(createAssetsAccountName(paymentAccount, xmrAccountDelegate.isUsingSubAddresses() ? + xmrAccountDelegate.getMainAddress() : xmrAccountDelegate.getAddress())); + } + } + + @Override + public void addFormForEditAccount() { + gridRowFrom = gridRow; + addAccountNameTextFieldWithAutoFillToggleButton(); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"), + Res.get(xmrAccountDelegate.getAccount().getPaymentMethod().getId())); + final TradeCurrency singleTradeCurrency = xmrAccountDelegate.getAccount().getSingleTradeCurrency(); + final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : ""; + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.altcoin"), nameAndCode); + + if (xmrAccountDelegate.isUsingSubAddresses()) { + Tuple3 xmrMainAddress = addCompactTopLabelTextField(gridPane, ++gridRow, + Res.get("payment.altcoin.xmrMainAddress"), xmrAccountDelegate.getMainAddress()); + xmrMainAddress.second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.altcoin.xmrAccountIndex"), xmrAccountDelegate.getAccountIndex()) + .second.setMouseTransparent(false); + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.altcoin.initialXmrSubAddressIndex"), xmrAccountDelegate.getSubAddressIndex()) + .second.setMouseTransparent(false); + + Map> subAccountsByMainAddress = user.getSubAccountsById(); + List subAccounts = new ArrayList<>(subAccountsByMainAddress.getOrDefault(xmrAccountDelegate.getSubAccountId(), new HashSet<>())); + if (subAccounts.size() == 0) { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.altcoin.xmrSubAddress"), xmrAccountDelegate.getSubAddress()) + .second.setMouseTransparent(false); + } else { + StringBuilder subAddressReport = new StringBuilder(); + subAccounts.sort(Comparator.comparing(PaymentAccount::getCreationDate)); + for (PaymentAccount account : subAccounts) { + XmrAccountDelegate delegate = new XmrAccountDelegate((AssetAccount) account); + subAddressReport.append(Res.get("payment.altcoin.usedSubaddressList", + delegate.getSubAddressIndex(), + delegate.getSubAddress(), + Utilities.getShortId(delegate.getTradeId()))) + .append(System.lineSeparator()); + } + GridPane gridPane2 = new GridPane(); + gridPane2.getColumnConstraints().add(gridPane.getColumnConstraints().get(0)); + TitledPane titledPane = new TitledPane(Res.get("payment.altcoin.xmrSubAddressesUsed"), gridPane2); + titledPane.setExpanded(false); + gridPane.add(titledPane, 0, ++gridRow); + TextArea subAddressTextArea = new BisqTextArea(); + gridPane2.add(subAddressTextArea, 0, 1); + subAddressTextArea.setMinHeight(70); + subAddressTextArea.setText(subAddressReport.toString()); + subAddressTextArea.setEditable(false); + } + } else { + addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.altcoin.address"), xmrAccountDelegate.getAddress()) + .second.setMouseTransparent(false); + } + addLimitations(true); + } + + @Override + public void updateAllInputsValid() { + TradeCurrency selectedTradeCurrency = xmrAccountDelegate.getAccount().getSelectedTradeCurrency(); + if (selectedTradeCurrency != null) { + altCoinAddressValidator.setCurrencyCode(selectedTradeCurrency.getCode()); + if (xmrAccountDelegate.isUsingSubAddresses()) { + // monero using subaddresses + allInputsValid.set(isAccountNameValid() + && altCoinAddressValidator.validate(xmrAccountDelegate.getSubAddress()).isValid + && mainAddressValidator.validate(xmrAccountDelegate.getMainAddress()).isValid + && regexValidator.validate(xmrAccountDelegate.getPrivateViewKey()).isValid + && accountIndexValidator.validate(xmrAccountDelegate.getAccountIndex()).isValid + && subAddressIndexValidator.validate(xmrAccountDelegate.getSubAddressIndex()).isValid + && xmrAccountDelegate.getAccount().getSingleTradeCurrency() != null); + } else { + // fixed monero address + allInputsValid.set(isAccountNameValid() + && altCoinAddressValidator.validate(xmrAccountDelegate.getAddress()).isValid + && xmrAccountDelegate.getAccount().getSingleTradeCurrency() != null); + } + } + } + + private void maybeShowXmrSubAddressInfo() { + String key = "xmrSubAddressInfo"; + if (DontShowAgainLookup.showAgain(key)) { + new Popup() + .headLine(Res.get("account.altcoin.popup.xmr.subAddressHeadline")) + .attention(Res.get("account.altcoin.popup.xmr.subAddressInfo")) + .dontShowAgainId(key) + .show(); + } + } + + private static class XmrWalletAddressValidator extends InputValidator { + // enforce that the main wallet address uses PUBLIC_ADDRESS_PREFIX (not a subaddress) + private final CryptoNoteAddressValidator validator = new CryptoNoteAddressValidator(PUBLIC_ADDRESS_PREFIX); + + @Override + public ValidationResult validate(String input) { + AddressValidationResult adr = validator.validate(input); + return new ValidationResult(adr.isValid(), adr.getMessage()); + } + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.java b/desktop/src/main/java/bisq/desktop/main/MainView.java index 77a0c2154f..9158d35d94 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainView.java +++ b/desktop/src/main/java/bisq/desktop/main/MainView.java @@ -190,6 +190,7 @@ public class MainView extends InitializableView JFXBadge portfolioButtonWithBadge = new JFXBadge(portfolioButton); JFXBadge supportButtonWithBadge = new JFXBadge(supportButton); JFXBadge settingsButtonWithBadge = new JFXBadge(settingsButton); + JFXBadge accountButtonWithBadge = new JFXBadge(accountButton); JFXBadge daoButtonWithBadge = new JFXBadge(daoButton); Locale locale = GlobalSettings.getLocale(); @@ -322,7 +323,7 @@ public class MainView extends InitializableView HBox.setHgrow(primaryNav, Priority.SOMETIMES); HBox secondaryNav = new HBox(supportButtonWithBadge, getNavigationSpacer(), settingsButtonWithBadge, - getNavigationSpacer(), accountButton, getNavigationSpacer(), daoButtonWithBadge); + getNavigationSpacer(), accountButtonWithBadge, getNavigationSpacer(), daoButtonWithBadge); secondaryNav.getStyleClass().add("nav-secondary"); HBox.setHgrow(secondaryNav, Priority.SOMETIMES); @@ -369,6 +370,9 @@ public class MainView extends InitializableView setupBadge(settingsButtonWithBadge, new SimpleStringProperty(Res.get("shared.new")), model.getShowSettingsUpdatesNotification()); settingsButtonWithBadge.getStyleClass().add("new"); + setupBadge(accountButtonWithBadge, new SimpleStringProperty(Res.get("shared.new")), model.getShowAccountUpdatesNotification()); + accountButtonWithBadge.getStyleClass().add("new"); + setupBadge(daoButtonWithBadge, new SimpleStringProperty(Res.get("shared.new")), model.getShowDaoUpdatesNotification()); daoButtonWithBadge.getStyleClass().add("new"); diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index b4cae5695b..c1bcaa62d3 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -534,13 +534,11 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { if (p2PService.isBootstrapped()) { setupInvalidOpenOffersHandler(); - maybeShowXmrTxKeyReUseWarning(); } else { p2PService.addP2PServiceListener(new BootstrapListener() { @Override public void onUpdatedDataReceived() { setupInvalidOpenOffersHandler(); - maybeShowXmrTxKeyReUseWarning(); } }); } @@ -568,21 +566,6 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener { } } - private void maybeShowXmrTxKeyReUseWarning() { - String key = "xmrTxKeyReUse"; - // If we do not have a XMR account we set showAgain to true to avoid the popup at next restart because we show - // the information at account creation as well. - if (user.getPaymentAccounts() != null && - user.getPaymentAccounts().stream() - .noneMatch(a -> a.getSingleTradeCurrency() != null && a.getSingleTradeCurrency().getCode().equals("XMR"))) { - preferences.dontShowAgain(key, true); - } - - if (preferences.showAgain(key)) { - new Popup().information(Res.get("account.altcoin.popup.xmr.dataDirWarning")).dontShowAgainId(key).show(); - } - } - private void setupP2PNumPeersWatcher() { p2PService.getNumConnectedPeers().addListener((observable, oldValue, newValue) -> { int numPeers = (int) newValue; diff --git a/desktop/src/main/java/bisq/desktop/main/account/AccountView.java b/desktop/src/main/java/bisq/desktop/main/account/AccountView.java index 40993a913e..427f96c50f 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/AccountView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/AccountView.java @@ -266,12 +266,21 @@ public class AccountView extends ActivatableView { } String key = "accountPrivacyInfo"; - if (!DevEnv.isDevMode()) + if (DontShowAgainLookup.showAgain(key)) { + // for newbs: the welcome to your bisq account page new Popup() .headLine(Res.get("account.info.headline")) .backgroundInfo(Res.get("account.info.msg")) .dontShowAgainId(key) .show(); + } else { + // news badge leads to the XMR subaddress info page (added in v1.9.2) + new Popup() + .headLine(Res.get("account.altcoin.popup.xmr.dataDirWarningHeadline")) + .backgroundInfo(Res.get("account.altcoin.popup.xmr.dataDirWarning")) + .dontShowAgainId("accountSubAddressInfo") + .show(); + } } @Override diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java index 9d107e1d38..1060788f4b 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/altcoinaccounts/AltCoinAccountsView.java @@ -18,9 +18,11 @@ package bisq.desktop.main.account.content.altcoinaccounts; import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutocompleteComboBox; import bisq.desktop.components.TitledGroupBg; import bisq.desktop.components.paymentmethods.AssetsForm; import bisq.desktop.components.paymentmethods.PaymentMethodForm; +import bisq.desktop.components.paymentmethods.XmrForm; import bisq.desktop.main.account.content.PaymentAccountsView; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.FormBuilder; @@ -38,6 +40,7 @@ import bisq.core.payment.PaymentAccountFactory; import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.validation.AltCoinAddressValidator; import bisq.core.user.Preferences; +import bisq.core.user.User; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; import bisq.core.util.validation.InputValidator; @@ -55,6 +58,7 @@ import javax.inject.Named; import javafx.stage.Stage; import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.layout.GridPane; @@ -62,13 +66,15 @@ import javafx.scene.layout.VBox; import javafx.collections.ObservableList; +import javafx.util.StringConverter; + import java.util.Optional; -import static bisq.desktop.components.paymentmethods.AssetsForm.INSTANT_TRADE_NEWS; import static bisq.desktop.util.FormBuilder.add2ButtonsAfterGroup; import static bisq.desktop.util.FormBuilder.add3ButtonsAfterGroup; import static bisq.desktop.util.FormBuilder.addTitledGroupBg; import static bisq.desktop.util.FormBuilder.addTopLabelListView; +import static bisq.desktop.util.GUIUtil.getComboBoxButtonCell; @FxmlView public class AltCoinAccountsView extends PaymentAccountsView { @@ -78,12 +84,14 @@ public class AltCoinAccountsView extends PaymentAccountsView currencyComboBox; @Inject public AltCoinAccountsView(AltCoinAccountsViewModel model, @@ -93,6 +101,7 @@ public class AltCoinAccountsView extends PaymentAccountsView tuple2 = add2ButtonsAfterGroup(root, ++gridRow, Res.get("shared.saveNewAccount"), Res.get("shared.cancel")); @@ -262,9 +270,23 @@ public class AltCoinAccountsView extends PaymentAccountsViewaddLabelAutocompleteComboBox(gridPane, ++gridRow, Res.get("payment.altcoin"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + currencyComboBox.setPromptText(Res.get("payment.select.altcoin")); + currencyComboBox.setButtonCell(getComboBoxButtonCell(Res.get("payment.select.altcoin"), currencyComboBox)); + + currencyComboBox.getEditor().focusedProperty().addListener(observable -> + currencyComboBox.setPromptText("")); + + ((AutocompleteComboBox) currencyComboBox).setAutocompleteItems( + CurrencyUtil.getActiveSortedCryptoCurrencies(assetService, filterManager)); + currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 10)); + + currencyComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(TradeCurrency tradeCurrency) { + return tradeCurrency != null ? tradeCurrency.getNameAndCode() : ""; + } + + @Override + public TradeCurrency fromString(String s) { + return currencyComboBox.getItems().stream(). + filter(item -> item.getNameAndCode().equals(s)). + findAny().orElse(null); + } + }); + + if (selectedCurrency != null) { + currencyComboBox.setValue(selectedCurrency); + } + ((AutocompleteComboBox) currencyComboBox).setOnChangeConfirmed(e -> { + if (currencyComboBox.getValue() != null) { + addNewAccount(); + } + }); + } + + } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java b/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java index b9f33a5e38..955fc9fcd7 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java @@ -147,9 +147,6 @@ public class DaoView extends ActivatableView { if (preferences.showAgain(DaoPresentation.DAO_NEWS)) { preferences.dontShowAgain(DaoPresentation.DAO_NEWS, true); - new Popup().headLine(Res.get("news.bsqSwap.title")) - .information(Res.get("news.bsqSwap.description")) - .show(); } navigation.addListener(navigationListener); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index 46036e62e7..97cbc7d4f7 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -685,7 +685,7 @@ public abstract class TradeStepView extends AnchorPane { } protected boolean isXmrTrade() { - return getCurrencyCode(trade).equals("XMR"); + return checkNotNull(trade.getOffer()).isXmr(); } private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java index 2d9c46e5b5..5b24b48cff 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/seller/SellerStep3View.java @@ -225,10 +225,8 @@ public class SellerStep3View extends TradeStepView { .orElse(""); if (myPaymentAccountPayload instanceof AssetsAccountPayload) { - if (myPaymentDetails.isEmpty()) { - // Not expected - myPaymentDetails = ((AssetsAccountPayload) myPaymentAccountPayload).getAddress(); - } + // for altcoins always display the receiving address + myPaymentDetails = ((AssetsAccountPayload) myPaymentAccountPayload).getAddress(); peersPaymentDetails = peersPaymentAccountPayload != null ? ((AssetsAccountPayload) peersPaymentAccountPayload).getAddress() : "NA"; myTitle = Res.get("portfolio.pending.step3_seller.yourAddress", currencyName); diff --git a/desktop/src/main/java/bisq/desktop/main/presentation/AccountPresentation.java b/desktop/src/main/java/bisq/desktop/main/presentation/AccountPresentation.java index f7af11f387..3cf3a0ae86 100644 --- a/desktop/src/main/java/bisq/desktop/main/presentation/AccountPresentation.java +++ b/desktop/src/main/java/bisq/desktop/main/presentation/AccountPresentation.java @@ -37,7 +37,7 @@ import javafx.collections.MapChangeListener; @Singleton public class AccountPresentation { - public static final String ACCOUNT_NEWS = "accountNews"; + public static final String ACCOUNT_NEWS = "accountNews_XmrSubAddresses"; private Preferences preferences; @@ -64,6 +64,7 @@ public class AccountPresentation { } public void setup() { + // devs enable this when a news badge is required showNotification.set(preferences.showAgain(ACCOUNT_NEWS)); } diff --git a/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java b/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java index 05fe6d0406..b1a6791ec3 100644 --- a/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java +++ b/desktop/src/main/java/bisq/desktop/main/presentation/DaoPresentation.java @@ -31,7 +31,7 @@ import lombok.Getter; @Singleton public class DaoPresentation implements DaoStateListener { - public static final String DAO_NEWS = "daoNews_BsqSwaps"; + public static final String DAO_NEWS = "daoNews"; private final Preferences preferences; private final Navigation navigation; @@ -65,7 +65,9 @@ public class DaoPresentation implements DaoStateListener { preferences.getDontShowAgainMapAsObservable().addListener((MapChangeListener) change -> { if (change.getKey().equals(DAO_NEWS) && DevEnv.isDaoActivated()) { - showNotification.set(!change.wasAdded()); + // devs enable this when a news badge is required + // showNotification.set(!change.wasAdded()); + showNotification.set(false); } }); @@ -127,8 +129,9 @@ public class DaoPresentation implements DaoStateListener { } public void setup() { - if (DevEnv.isDaoActivated()) - showNotification.set(preferences.showAgain(DAO_NEWS)); + // devs enable this when a news badge is required + //showNotification.set(DevEnv.isDaoActivated() && preferences.showAgain(DAO_NEWS)); + showNotification.set(false); this.btcWalletService.getChainHeightProperty().addListener(walletChainHeightListener); daoStateService.addDaoStateListener(this); diff --git a/desktop/src/main/java/bisq/desktop/main/presentation/SettingsPresentation.java b/desktop/src/main/java/bisq/desktop/main/presentation/SettingsPresentation.java index f1eab5c2f9..9e6c6c6042 100644 --- a/desktop/src/main/java/bisq/desktop/main/presentation/SettingsPresentation.java +++ b/desktop/src/main/java/bisq/desktop/main/presentation/SettingsPresentation.java @@ -31,7 +31,7 @@ import javafx.collections.MapChangeListener; @Singleton public class SettingsPresentation { - public static final String SETTINGS_BADGE_KEY = "settingsPrivacyFeature"; + public static final String SETTINGS_BADGE_KEY = "settingsNews"; private Preferences preferences; @@ -44,7 +44,9 @@ public class SettingsPresentation { preferences.getDontShowAgainMapAsObservable().addListener((MapChangeListener) change -> { if (change.getKey().equals(SETTINGS_BADGE_KEY)) { - showNotification.set(!change.wasAdded()); + // devs enable this when a news badge is required + // showNotification.set(!change.wasAdded()); + showNotification.set(false); } }); } @@ -58,6 +60,8 @@ public class SettingsPresentation { } public void setup() { - showNotification.set(preferences.showAgain(SETTINGS_BADGE_KEY)); + // devs enable this when a news badge is required + // showNotification.set(preferences.showAgain(SETTINGS_BADGE_KEY)); + showNotification.set(false); } } diff --git a/desktop/src/main/java/bisq/desktop/main/settings/SettingsView.java b/desktop/src/main/java/bisq/desktop/main/settings/SettingsView.java index 771df416cd..1e636227a3 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/SettingsView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/SettingsView.java @@ -24,14 +24,12 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.common.view.View; import bisq.desktop.common.view.ViewLoader; import bisq.desktop.main.MainView; -import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.presentation.SettingsPresentation; import bisq.desktop.main.settings.about.AboutView; import bisq.desktop.main.settings.network.NetworkSettingsView; import bisq.desktop.main.settings.preferences.PreferencesView; import bisq.core.locale.Res; -import bisq.core.user.DontShowAgainLookup; import bisq.core.user.Preferences; import javax.inject.Inject; diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index d12eec46fc..e51d4b293f 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -226,7 +226,8 @@ public class PreferencesView extends ActivatableViewAndModel cookie = 16; + repeated SubAccountMapEntry sub_account_map_entries = 17; +} + +message SubAccountMapEntry { + string key = 1; + repeated PaymentAccount value = 2; } message BaseBlock { @@ -2414,6 +2421,7 @@ message PaymentAccount { repeated TradeCurrency trade_currencies = 5; TradeCurrency selected_trade_currency = 6; PaymentAccountPayload payment_account_payload = 7; + map extra_data = 8; } message PaymentMethod {