Feat: XMR subaddresses per account

incremente subaddress index at the start of the trade

Add subaccounts

Rename XmrAccountHelper to XmrAccountDelegate
Add map as final non null value

Persist subaccounts in User

Add display of used subaddresses and tradeId in account summary.
Main address and account index are the unique key for sub accounts.
Use the initial subaddress chosen by the user.

chimp1984 code review patch.

News badge/info for Account, disable old ones.

Show XMR subaddress popup info at Account (news badge), not startup.
This commit is contained in:
jmacxx 2022-06-05 11:24:44 -05:00
parent a62655c15d
commit 920e05562c
No known key found for this signature in database
GPG Key ID: 155297BABFE94A1B
27 changed files with 925 additions and 133 deletions

View File

@ -47,4 +47,8 @@ public final class CryptoCurrencyAccount extends AssetAccount {
public @NonNull List<TradeCurrency> getSupportedCurrencies() {
return SUPPORTED_CURRENCIES;
}
public PaymentAccountPayload getPaymentAccountPayload() {
return paymentAccountPayload;
}
}

View File

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

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

View File

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

View File

@ -67,6 +67,13 @@ public class DisputeProtocol extends TradeProtocol {
this.processModel = trade.getProcessModel();
}
@Override
protected void onInitialized() {
super.onInitialized();
processModel.applyPaymentAccount(trade);
}
///////////////////////////////////////////////////////////////////////////////////////////
// TradeProtocol implementation
///////////////////////////////////////////////////////////////////////////////////////////

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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