Merge pull request #6236 from jmacxx/xmr_subaddresses

Feat: XMR subaddresses per account
This commit is contained in:
Christoph Atteneder 2022-06-20 20:40:20 +02:00 committed by GitHub
commit 444f2183e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 922 additions and 160 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\
@ -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].

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

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

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 {