mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 01:41:11 +01:00
Merge pull request #6236 from jmacxx/xmr_subaddresses
Feat: XMR subaddresses per account
This commit is contained in:
commit
444f2183e5
@ -47,4 +47,8 @@ public final class CryptoCurrencyAccount extends AssetAccount {
|
||||
public @NonNull List<TradeCurrency> getSupportedCurrencies() {
|
||||
return SUPPORTED_CURRENCIES;
|
||||
}
|
||||
|
||||
public PaymentAccountPayload getPaymentAccountPayload() {
|
||||
return paymentAccountPayload;
|
||||
}
|
||||
}
|
||||
|
@ -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<String, String> 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<TradeCurrency> getSupportedCurrencies();
|
||||
|
||||
public Map<String, String> getOrCreateExtraData() {
|
||||
if (extraData == null) {
|
||||
extraData = new HashMap<>();
|
||||
}
|
||||
return extraData;
|
||||
}
|
||||
}
|
||||
|
156
core/src/main/java/bisq/core/payment/XmrAccountDelegate.java
Normal file
156
core/src/main/java/bisq/core/payment/XmrAccountDelegate.java
Normal file
@ -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<String, String> getMap() {
|
||||
return account.getOrCreateExtraData();
|
||||
}
|
||||
}
|
@ -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<TradableList<Trade>> 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));
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +67,13 @@ public class DisputeProtocol extends TradeProtocol {
|
||||
this.processModel = trade.getProcessModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInitialized() {
|
||||
super.onInitialized();
|
||||
processModel.applyPaymentAccount(trade);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// TradeProtocol implementation
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
|
@ -118,7 +118,6 @@ public class ProcessModel implements ProtocolModel<TradingPeer> {
|
||||
// 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<TradingPeer> {
|
||||
@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<TradingPeer> {
|
||||
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<TradingPeer> {
|
||||
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<TradingPeer> {
|
||||
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<TradingPeer> {
|
||||
|
||||
@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() {
|
||||
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Trade> 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<String, Set<PaymentAccount>> subAccountsBySubAccountId = processModel.getUser().getSubAccountsById();
|
||||
subAccountsBySubAccountId.putIfAbsent(xmrAccountDelegate.getSubAccountId(), new HashSet<>());
|
||||
Set<PaymentAccount> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<String, Set<PaymentAccount>> getSubAccountsById() {
|
||||
return userPayload.getSubAccountsById();
|
||||
}
|
||||
}
|
||||
|
@ -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<RefundAgent> 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<String, Set<PaymentAccount>> subAccountsById = new HashMap<>();
|
||||
|
||||
public UserPayload() {
|
||||
}
|
||||
|
||||
public UserPayload(String accountId,
|
||||
Set<PaymentAccount> paymentAccounts,
|
||||
PaymentAccount currentPaymentAccount,
|
||||
List<String> acceptedLanguageLocaleCodes,
|
||||
Alert developersAlert,
|
||||
Alert displayedAlert,
|
||||
Filter developersFilter,
|
||||
Arbitrator registeredArbitrator,
|
||||
Mediator registeredMediator,
|
||||
List<Arbitrator> acceptedArbitrators,
|
||||
List<Mediator> acceptedMediators,
|
||||
PriceAlertFilter priceAlertFilter,
|
||||
List<MarketAlertFilter> marketAlertFilters,
|
||||
RefundAgent registeredRefundAgent,
|
||||
List<RefundAgent> acceptedRefundAgents,
|
||||
Cookie cookie,
|
||||
Map<String, Set<PaymentAccount>> 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<String, Set<PaymentAccount>> 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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\
|
||||
@ -4287,21 +4304,3 @@ validation.phone.invalidDialingCode=Country dialing code for number {0} is inval
|
||||
validation.invalidAddressList=Must be comma separated list of valid addresses
|
||||
validation.capitual.invalidFormat=Must be a valid CAP code of format: CAP-XXXXXX (6 alphanumeric characters)
|
||||
|
||||
|
||||
####################################################################
|
||||
# News
|
||||
####################################################################
|
||||
|
||||
news.bsqSwap.title=New trade protocol: BSQ SWAPS
|
||||
news.bsqSwap.description=BSQ swaps is a new trade protocol for atomically swapping BSQ and BTC in a single \
|
||||
transaction.\n\n\
|
||||
This saves miner fees, allows instant trades, removes counterparty risk, and does not require \
|
||||
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].
|
||||
|
@ -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.<TradeCurrency>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<TradeCurrency>) 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Label, TextField, VBox> 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<String, Set<PaymentAccount>> subAccountsByMainAddress = user.getSubAccountsById();
|
||||
List<PaymentAccount> 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());
|
||||
}
|
||||
}
|
||||
}
|
@ -190,6 +190,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel>
|
||||
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<StackPane, MainViewModel>
|
||||
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<StackPane, MainViewModel>
|
||||
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");
|
||||
|
||||
|
@ -323,7 +323,6 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
|
||||
setupDevDummyPaymentAccounts();
|
||||
}
|
||||
|
||||
maybeAddMediationRulesAwarenessWindowToQueue();
|
||||
getShowAppScreen().set(true);
|
||||
}
|
||||
|
||||
@ -534,13 +533,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 +565,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;
|
||||
@ -907,20 +889,6 @@ public class MainViewModel implements ViewModel, BisqSetup.BisqSetupListener {
|
||||
return settingsPresentation.getShowSettingsUpdatesNotification();
|
||||
}
|
||||
|
||||
private void maybeAddMediationRulesAwarenessWindowToQueue() {
|
||||
String key = "mediationRulesAwarenessPopup";
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
Popup popup = new Popup()
|
||||
.headLine(Res.get("news.mediationRules.title"))
|
||||
.information(Res.get("news.mediationRules.info"))
|
||||
.actionButtonText(Res.get("shared.iUnderstand"))
|
||||
.hideCloseButton()
|
||||
.dontShowAgainId(key);
|
||||
popup.setDisplayOrderPriority(1);
|
||||
popupQueue.add(popup);
|
||||
}
|
||||
}
|
||||
|
||||
private void maybeShowPopupsFromQueue() {
|
||||
if (!popupQueue.isEmpty()) {
|
||||
Overlay<?> overlay = popupQueue.poll();
|
||||
|
@ -266,12 +266,21 @@ public class AccountView extends ActivatableView<TabPane, Void> {
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -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<GridPane, AltCoinAccountsViewModel> {
|
||||
@ -78,12 +84,14 @@ public class AltCoinAccountsView extends PaymentAccountsView<GridPane, AltCoinAc
|
||||
private final AssetService assetService;
|
||||
private final FilterManager filterManager;
|
||||
private final CoinFormatter formatter;
|
||||
private final User user;
|
||||
private final Preferences preferences;
|
||||
|
||||
private PaymentMethodForm paymentMethodForm;
|
||||
private TitledGroupBg accountTitledGroupBg;
|
||||
private Button saveNewAccountButton;
|
||||
private int gridRow = 0;
|
||||
protected ComboBox<TradeCurrency> currencyComboBox;
|
||||
|
||||
@Inject
|
||||
public AltCoinAccountsView(AltCoinAccountsViewModel model,
|
||||
@ -93,6 +101,7 @@ public class AltCoinAccountsView extends PaymentAccountsView<GridPane, AltCoinAc
|
||||
AssetService assetService,
|
||||
FilterManager filterManager,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter,
|
||||
User user,
|
||||
Preferences preferences) {
|
||||
super(model, accountAgeWitnessService);
|
||||
|
||||
@ -101,6 +110,7 @@ public class AltCoinAccountsView extends PaymentAccountsView<GridPane, AltCoinAc
|
||||
this.assetService = assetService;
|
||||
this.filterManager = filterManager;
|
||||
this.formatter = formatter;
|
||||
this.user = user;
|
||||
this.preferences = preferences;
|
||||
}
|
||||
|
||||
@ -157,15 +167,11 @@ public class AltCoinAccountsView extends PaymentAccountsView<GridPane, AltCoinAc
|
||||
} else {
|
||||
new Popup().warning(Res.get("shared.accountNameAlreadyUsed")).show();
|
||||
}
|
||||
|
||||
preferences.dontShowAgain(INSTANT_TRADE_NEWS, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void onCancelNewAccount() {
|
||||
removeNewAccountForm();
|
||||
|
||||
preferences.dontShowAgain(INSTANT_TRADE_NEWS, true);
|
||||
}
|
||||
|
||||
private void onUpdateAccount(PaymentAccount paymentAccount) {
|
||||
@ -201,6 +207,7 @@ public class AltCoinAccountsView extends PaymentAccountsView<GridPane, AltCoinAc
|
||||
// Add new account form
|
||||
protected void addNewAccount() {
|
||||
paymentAccountsListView.getSelectionModel().clearSelection();
|
||||
TradeCurrency selectedCurrency = currencyComboBox == null ? null : currencyComboBox.getValue();
|
||||
removeAccountRows();
|
||||
addAccountButton.setDisable(true);
|
||||
accountTitledGroupBg = addTitledGroupBg(root, ++gridRow, 1, Res.get("shared.createNewAccount"), Layout.GROUP_DISTANCE);
|
||||
@ -210,7 +217,8 @@ public class AltCoinAccountsView extends PaymentAccountsView<GridPane, AltCoinAc
|
||||
GridPane.setRowSpan(accountTitledGroupBg, paymentMethodForm.getRowSpan() + 1);
|
||||
}
|
||||
gridRow = 2;
|
||||
paymentMethodForm = getPaymentMethodForm(PaymentMethod.BLOCK_CHAINS);
|
||||
addTradeCurrencyComboBox(root, selectedCurrency);
|
||||
paymentMethodForm = getPaymentMethodForm(PaymentMethod.BLOCK_CHAINS, selectedCurrency);
|
||||
paymentMethodForm.addFormForAddAccount();
|
||||
gridRow = paymentMethodForm.getGridRow();
|
||||
Tuple2<Button, Button> tuple2 = add2ButtonsAfterGroup(root, ++gridRow, Res.get("shared.saveNewAccount"), Res.get("shared.cancel"));
|
||||
@ -262,9 +270,23 @@ public class AltCoinAccountsView extends PaymentAccountsView<GridPane, AltCoinAc
|
||||
return getPaymentMethodForm(paymentAccount);
|
||||
}
|
||||
|
||||
private PaymentMethodForm getPaymentMethodForm(PaymentMethod paymentMethod, TradeCurrency currencyCode) {
|
||||
PaymentAccount paymentAccount = PaymentAccountFactory.getPaymentAccount(paymentMethod);
|
||||
paymentAccount.init();
|
||||
paymentAccount.setSingleTradeCurrency(currencyCode);
|
||||
paymentAccount.setSelectedTradeCurrency(currencyCode);
|
||||
return getPaymentMethodForm(paymentAccount);
|
||||
}
|
||||
|
||||
private PaymentMethodForm getPaymentMethodForm(PaymentAccount paymentAccount) {
|
||||
if (paymentAccount.getSelectedTradeCurrency() != null &&
|
||||
paymentAccount.getSelectedTradeCurrency().getCode() != null &&
|
||||
paymentAccount.getSelectedTradeCurrency().getCode().equalsIgnoreCase("XMR")) {
|
||||
return new XmrForm(paymentAccount, accountAgeWitnessService, altCoinAddressValidator,
|
||||
inputValidator, root, gridRow, formatter, assetService, user);
|
||||
}
|
||||
return new AssetsForm(paymentAccount, accountAgeWitnessService, altCoinAddressValidator,
|
||||
inputValidator, root, gridRow, formatter, assetService, filterManager);
|
||||
inputValidator, root, gridRow, formatter, assetService);
|
||||
}
|
||||
|
||||
private void removeNewAccountForm() {
|
||||
@ -290,4 +312,43 @@ public class AltCoinAccountsView extends PaymentAccountsView<GridPane, AltCoinAc
|
||||
FormBuilder.removeRowsFromGridPane(root, 2, gridRow);
|
||||
gridRow = 1;
|
||||
}
|
||||
|
||||
protected void addTradeCurrencyComboBox(GridPane gridPane, TradeCurrency selectedCurrency) {
|
||||
currencyComboBox = FormBuilder.<TradeCurrency>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<TradeCurrency>) 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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -147,9 +147,6 @@ public class DaoView extends ActivatableView<TabPane, Void> {
|
||||
|
||||
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);
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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<? super String, ? super Boolean>) 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);
|
||||
|
@ -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<? super String, ? super Boolean>) 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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -226,7 +226,8 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
|
||||
@Override
|
||||
protected void activate() {
|
||||
String key = "sensitiveDataRemovalInfo";
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
if (DontShowAgainLookup.showAgain(key) &&
|
||||
preferences.getClearDataAfterDays() == preferences.CLEAR_DATA_AFTER_DAYS_INITIAL) {
|
||||
new Popup()
|
||||
.headLine(Res.get("setting.info.headline"))
|
||||
.backgroundInfo(Res.get("settings.preferences.sensitiveDataRemoval.msg"))
|
||||
|
@ -1808,6 +1808,7 @@ message ProcessModel {
|
||||
bytes mediated_payout_tx_signature = 18;
|
||||
int64 buyer_payout_amount_from_mediation = 19;
|
||||
int64 seller_payout_amount_from_mediation = 20;
|
||||
PaymentAccount payment_account = 21;
|
||||
}
|
||||
|
||||
message TradingPeer {
|
||||
@ -1981,6 +1982,12 @@ message UserPayload {
|
||||
repeated RefundAgent accepted_refund_agents = 14;
|
||||
RefundAgent registered_refund_agent = 15;
|
||||
map<string, string> 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<string, string> extra_data = 8;
|
||||
}
|
||||
|
||||
message PaymentMethod {
|
||||
|
Loading…
Reference in New Issue
Block a user