diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index af939e6555a..606121d8877 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -73,7 +73,7 @@
-
+
diff --git a/src/main/java/bisq/desktop/bisq.css b/src/main/java/bisq/desktop/bisq.css
index d1485bf1e6d..a01697a1346 100644
--- a/src/main/java/bisq/desktop/bisq.css
+++ b/src/main/java/bisq/desktop/bisq.css
@@ -553,6 +553,20 @@ textfield */
-fx-text-fill: -bs-medium-grey;
}
+.delete-icon {
+ -fx-fill: -bs-red;
+}
+
+.delete {
+ -fx-text-fill: -bs-error-red;
+ -fx-fill: -bs-error-red;
+}
+
+.delete:hover {
+ -fx-text-fill: -bs-black;
+ -fx-fill: -bs-black;
+}
+
/*******************************************************************************
* *
* Images *
diff --git a/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java b/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java
index fe26b0ec9b7..facc467e88f 100644
--- a/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java
+++ b/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java
@@ -19,6 +19,8 @@
import bisq.common.UserThread;
+import de.jensd.fx.glyphs.GlyphIcons;
+
import org.controlsfx.control.PopOver;
import javafx.scene.Node;
@@ -32,10 +34,6 @@
import static bisq.desktop.util.FormBuilder.getIcon;
-
-
-import de.jensd.fx.glyphs.GlyphIcons;
-
public class InfoAutoTooltipLabel extends AutoTooltipLabel {
private Text textIcon;
diff --git a/src/main/java/bisq/desktop/main/offer/EditableOfferDataModel.java b/src/main/java/bisq/desktop/main/offer/EditableOfferDataModel.java
new file mode 100644
index 00000000000..9f90189d125
--- /dev/null
+++ b/src/main/java/bisq/desktop/main/offer/EditableOfferDataModel.java
@@ -0,0 +1,792 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.desktop.main.offer;
+
+import bisq.desktop.util.BSFormatter;
+
+import bisq.core.app.BisqEnvironment;
+import bisq.core.arbitration.Arbitrator;
+import bisq.core.btc.AddressEntry;
+import bisq.core.btc.Restrictions;
+import bisq.core.btc.listeners.BalanceListener;
+import bisq.core.btc.wallet.BsqBalanceListener;
+import bisq.core.btc.wallet.BsqWalletService;
+import bisq.core.btc.wallet.BtcWalletService;
+import bisq.core.btc.wallet.TradeWalletService;
+import bisq.core.filter.FilterManager;
+import bisq.core.locale.CurrencyUtil;
+import bisq.core.locale.Res;
+import bisq.core.locale.TradeCurrency;
+import bisq.core.monetary.Price;
+import bisq.core.monetary.Volume;
+import bisq.core.offer.Offer;
+import bisq.core.offer.OfferPayload;
+import bisq.core.offer.OfferUtil;
+import bisq.core.offer.OpenOfferManager;
+import bisq.core.payment.AccountAgeWitnessService;
+import bisq.core.payment.BankAccount;
+import bisq.core.payment.CountryBasedPaymentAccount;
+import bisq.core.payment.PaymentAccount;
+import bisq.core.payment.SameBankAccount;
+import bisq.core.payment.SepaAccount;
+import bisq.core.payment.SepaInstantAccount;
+import bisq.core.payment.SpecificBanksAccount;
+import bisq.core.provider.fee.FeeService;
+import bisq.core.provider.price.PriceFeedService;
+import bisq.core.trade.handlers.TransactionResultHandler;
+import bisq.core.user.Preferences;
+import bisq.core.user.User;
+
+import bisq.network.p2p.P2PService;
+
+import bisq.common.app.Version;
+import bisq.common.crypto.KeyRing;
+import bisq.common.util.Utilities;
+
+import org.bitcoinj.core.Address;
+import org.bitcoinj.core.Coin;
+import org.bitcoinj.core.InsufficientMoneyException;
+import org.bitcoinj.core.Transaction;
+
+import com.google.inject.Inject;
+
+import com.google.common.collect.Lists;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyObjectProperty;
+import javafx.beans.property.ReadOnlyStringProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.SetChangeListener;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public abstract class EditableOfferDataModel extends OfferDataModel implements BsqBalanceListener {
+ protected final OpenOfferManager openOfferManager;
+ private final BsqWalletService bsqWalletService;
+ private final Preferences preferences;
+ protected final User user;
+ private final KeyRing keyRing;
+ private final P2PService p2PService;
+ protected final PriceFeedService priceFeedService;
+ final String shortOfferId;
+ private final FilterManager filterManager;
+ private final AccountAgeWitnessService accountAgeWitnessService;
+ private final TradeWalletService tradeWalletService;
+ private final FeeService feeService;
+ private final BSFormatter formatter;
+ private final String offerId;
+ private final BalanceListener btcBalanceListener;
+ private final SetChangeListener paymentAccountsChangeListener;
+
+ private OfferPayload.Direction direction;
+ private TradeCurrency tradeCurrency;
+ private final StringProperty tradeCurrencyCode = new SimpleStringProperty();
+ private final BooleanProperty useMarketBasedPrice = new SimpleBooleanProperty();
+ //final BooleanProperty isMainNet = new SimpleBooleanProperty();
+ //final BooleanProperty isFeeFromFundingTxSufficient = new SimpleBooleanProperty();
+
+ // final ObjectProperty feeFromFundingTxProperty = new SimpleObjectProperty(Coin.NEGATIVE_SATOSHI);
+ private final ObjectProperty amount = new SimpleObjectProperty<>();
+ private final ObjectProperty minAmount = new SimpleObjectProperty<>();
+ private final ObjectProperty price = new SimpleObjectProperty<>();
+ private final ObjectProperty volume = new SimpleObjectProperty<>();
+ private final ObjectProperty buyerSecurityDeposit = new SimpleObjectProperty<>();
+ private final Coin sellerSecurityDeposit;
+
+ private final ObservableList paymentAccounts = FXCollections.observableArrayList();
+
+ protected PaymentAccount paymentAccount;
+ boolean isTabSelected;
+ private double marketPriceMargin = 0;
+ private Coin txFeeFromFeeService;
+ private boolean marketPriceAvailable;
+ private int feeTxSize = 260; // size of typical tx with 1 input
+ private int feeTxSizeEstimationRecursionCounter;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Constructor, lifecycle
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ @Inject
+ public EditableOfferDataModel(OpenOfferManager openOfferManager, BtcWalletService btcWalletService, BsqWalletService bsqWalletService,
+ Preferences preferences, User user, KeyRing keyRing, P2PService p2PService,
+ PriceFeedService priceFeedService, FilterManager filterManager,
+ AccountAgeWitnessService accountAgeWitnessService, TradeWalletService tradeWalletService,
+ FeeService feeService, BSFormatter formatter) {
+ super(btcWalletService);
+
+ this.openOfferManager = openOfferManager;
+ this.bsqWalletService = bsqWalletService;
+ this.preferences = preferences;
+ this.user = user;
+ this.keyRing = keyRing;
+ this.p2PService = p2PService;
+ this.priceFeedService = priceFeedService;
+ this.filterManager = filterManager;
+ this.accountAgeWitnessService = accountAgeWitnessService;
+ this.tradeWalletService = tradeWalletService;
+ this.feeService = feeService;
+ this.formatter = formatter;
+
+ offerId = Utilities.getRandomPrefix(5, 8) + "-" +
+ UUID.randomUUID().toString() + "-" +
+ Version.VERSION.replace(".", "");
+ shortOfferId = Utilities.getShortId(offerId);
+ addressEntry = btcWalletService.getOrCreateAddressEntry(offerId, AddressEntry.Context.OFFER_FUNDING);
+
+ useMarketBasedPrice.set(preferences.isUsePercentageBasedPrice());
+ buyerSecurityDeposit.set(preferences.getBuyerSecurityDepositAsCoin());
+ sellerSecurityDeposit = Restrictions.getSellerSecurityDeposit();
+
+ btcBalanceListener = new BalanceListener(getAddressEntry().getAddress()) {
+ @Override
+ public void onBalanceChanged(Coin balance, Transaction tx) {
+ updateBalance();
+
+ /* if (isMainNet.get()) {
+ SettableFuture future = blockchainService.requestFee(tx.getHashAsString());
+ Futures.addCallback(future, new FutureCallback() {
+ public void onSuccess(Coin fee) {
+ UserThread.execute(() -> feeFromFundingTxProperty.set(fee));
+ }
+
+ public void onFailure(@NotNull Throwable throwable) {
+ UserThread.execute(() -> new Popup<>()
+ .warning("We did not get a response for the request of the mining fee used " +
+ "in the funding transaction.\n\n" +
+ "Are you sure you used a sufficiently high fee of at least " +
+ formatter.formatCoinWithCode(FeePolicy.getMinRequiredFeeForFundingTx()) + "?")
+ .actionButtonText("Yes, I used a sufficiently high fee.")
+ .onAction(() -> feeFromFundingTxProperty.set(FeePolicy.getMinRequiredFeeForFundingTx()))
+ .closeButtonText("No. Let's cancel that payment.")
+ .onClose(() -> feeFromFundingTxProperty.set(Coin.ZERO))
+ .show());
+ }
+ });
+ }*/
+ }
+ };
+
+ paymentAccountsChangeListener = change -> fillPaymentAccounts();
+ }
+
+ @Override
+ public void activate() {
+ addListeners();
+
+ if (isTabSelected)
+ priceFeedService.setCurrencyCode(tradeCurrencyCode.get());
+
+ updateBalance();
+ }
+
+ @Override
+ protected void deactivate() {
+ removeListeners();
+ }
+
+ private void addListeners() {
+ btcWalletService.addBalanceListener(btcBalanceListener);
+ if (BisqEnvironment.isBaseCurrencySupportingBsq())
+ bsqWalletService.addBsqBalanceListener(this);
+ user.getPaymentAccountsAsObservable().addListener(paymentAccountsChangeListener);
+ }
+
+
+ private void removeListeners() {
+ btcWalletService.removeBalanceListener(btcBalanceListener);
+ if (BisqEnvironment.isBaseCurrencySupportingBsq())
+ bsqWalletService.removeBsqBalanceListener(this);
+ user.getPaymentAccountsAsObservable().removeListener(paymentAccountsChangeListener);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // API
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ // called before activate()
+ public boolean initWithData(OfferPayload.Direction direction, TradeCurrency tradeCurrency) {
+ this.direction = direction;
+ this.tradeCurrency = tradeCurrency;
+
+ fillPaymentAccounts();
+
+ PaymentAccount account;
+ PaymentAccount lastSelectedPaymentAccount = preferences.getSelectedPaymentAccountForCreateOffer();
+ if (lastSelectedPaymentAccount != null &&
+ user.getPaymentAccounts() != null &&
+ user.getPaymentAccounts().contains(lastSelectedPaymentAccount)) {
+ account = lastSelectedPaymentAccount;
+ } else {
+ account = user.findFirstPaymentAccountWithCurrency(tradeCurrency);
+ }
+
+ if (account != null) {
+ this.paymentAccount = account;
+ } else {
+ Optional paymentAccountOptional = paymentAccounts.stream().findAny();
+ if (paymentAccountOptional.isPresent()) {
+ this.paymentAccount = paymentAccountOptional.get();
+
+ } else {
+ log.warn("PaymentAccount not available. Should never get called as in offer view you should not be able to open a create offer view");
+ return false;
+ }
+ }
+
+ setTradeCurrencyFromPaymentAccount(paymentAccount);
+ tradeCurrencyCode.set(this.tradeCurrency.getCode());
+
+ priceFeedService.setCurrencyCode(tradeCurrencyCode.get());
+
+ // We request to get the actual estimated fee
+ requestTxFee();
+
+ // Set the default values (in rare cases if the fee request was not done yet we get the hard coded default values)
+ // But offer creation happens usually after that so we should have already the value from the estimation service.
+ txFeeFromFeeService = feeService.getTxFee(feeTxSize);
+
+ calculateVolume();
+ calculateTotalToPay();
+ updateBalance();
+
+ return true;
+ }
+
+ void onTabSelected(boolean isSelected) {
+ this.isTabSelected = isSelected;
+ if (isTabSelected)
+ priceFeedService.setCurrencyCode(tradeCurrencyCode.get());
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // UI actions
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ @SuppressWarnings("ConstantConditions")
+ Offer createAndGetOffer() {
+ final boolean useMarketBasedPriceValue = isUseMarketBasedPriceValue();
+ long priceAsLong = price.get() != null && !useMarketBasedPriceValue ? price.get().getValue() : 0L;
+ String currencyCode = tradeCurrencyCode.get();
+ boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode);
+ String baseCurrencyCode = isCryptoCurrency ? currencyCode : Res.getBaseCurrencyCode();
+ String counterCurrencyCode = isCryptoCurrency ? Res.getBaseCurrencyCode() : currencyCode;
+
+ double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
+ long amount = this.amount.get() != null ? this.amount.get().getValue() : 0L;
+ long minAmount = this.minAmount.get() != null ? this.minAmount.get().getValue() : 0L;
+
+ ArrayList acceptedCountryCodes = null;
+ if (paymentAccount instanceof SepaAccount) {
+ acceptedCountryCodes = new ArrayList<>();
+ acceptedCountryCodes.addAll(((SepaAccount) paymentAccount).getAcceptedCountryCodes());
+ } else if (paymentAccount instanceof SepaInstantAccount) {
+ acceptedCountryCodes = new ArrayList<>();
+ acceptedCountryCodes.addAll(((SepaInstantAccount) paymentAccount).getAcceptedCountryCodes());
+ } else if (paymentAccount instanceof CountryBasedPaymentAccount) {
+ acceptedCountryCodes = new ArrayList<>();
+ acceptedCountryCodes.add(((CountryBasedPaymentAccount) paymentAccount).getCountry().code);
+ }
+
+ ArrayList acceptedBanks = null;
+ if (paymentAccount instanceof SpecificBanksAccount) {
+ acceptedBanks = new ArrayList<>(((SpecificBanksAccount) paymentAccount).getAcceptedBanks());
+ } else if (paymentAccount instanceof SameBankAccount) {
+ acceptedBanks = new ArrayList<>();
+ acceptedBanks.add(((SameBankAccount) paymentAccount).getBankId());
+ }
+
+ String bankId = paymentAccount instanceof BankAccount ? ((BankAccount) paymentAccount).getBankId() : null;
+
+ // That is optional and set to null if not supported (AltCoins, OKPay,...)
+ String countryCode = paymentAccount instanceof CountryBasedPaymentAccount ? ((CountryBasedPaymentAccount) paymentAccount).getCountry().code : null;
+
+ checkNotNull(p2PService.getAddress(), "Address must not be null");
+ checkNotNull(getMakerFee(), "makerFee must not be null");
+
+ long maxTradeLimit = getMaxTradeLimit();
+ long maxTradePeriod = paymentAccount.getPaymentMethod().getMaxTradePeriod();
+
+ // reserved for future use cases
+ // Use null values if not set
+ boolean isPrivateOffer = false;
+ boolean useAutoClose = false;
+ boolean useReOpenAfterAutoClose = false;
+ long lowerClosePrice = 0;
+ long upperClosePrice = 0;
+ String hashOfChallenge = null;
+ HashMap extraDataMap = null;
+ if (CurrencyUtil.isFiatCurrency(currencyCode)) {
+ extraDataMap = new HashMap<>();
+ final String myWitnessHashAsHex = accountAgeWitnessService.getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload());
+ extraDataMap.put(OfferPayload.ACCOUNT_AGE_WITNESS_HASH, myWitnessHashAsHex);
+ }
+
+ Coin buyerSecurityDepositAsCoin = buyerSecurityDeposit.get();
+ checkArgument(buyerSecurityDepositAsCoin.compareTo(Restrictions.getMaxBuyerSecurityDeposit()) <= 0,
+ "securityDeposit must be not exceed " +
+ Restrictions.getMaxBuyerSecurityDeposit().toFriendlyString());
+ checkArgument(buyerSecurityDepositAsCoin.compareTo(Restrictions.getMinBuyerSecurityDeposit()) >= 0,
+ "securityDeposit must be not be less than " +
+ Restrictions.getMinBuyerSecurityDeposit().toFriendlyString());
+
+ checkArgument(!filterManager.isCurrencyBanned(currencyCode),
+ Res.get("offerbook.warning.currencyBanned"));
+ checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()),
+ Res.get("offerbook.warning.paymentMethodBanned"));
+
+ OfferPayload offerPayload = new OfferPayload(offerId,
+ new Date().getTime(),
+ p2PService.getAddress(),
+ keyRing.getPubKeyRing(),
+ OfferPayload.Direction.valueOf(direction.name()),
+ priceAsLong,
+ marketPriceMarginParam,
+ useMarketBasedPriceValue,
+ amount,
+ minAmount,
+ baseCurrencyCode,
+ counterCurrencyCode,
+ Lists.newArrayList(user.getAcceptedArbitratorAddresses()),
+ Lists.newArrayList(user.getAcceptedMediatorAddresses()),
+ paymentAccount.getPaymentMethod().getId(),
+ paymentAccount.getId(),
+ null,
+ countryCode,
+ acceptedCountryCodes,
+ bankId,
+ acceptedBanks,
+ Version.VERSION,
+ btcWalletService.getLastBlockSeenHeight(),
+ txFeeFromFeeService.value,
+ getMakerFee().value,
+ isCurrencyForMakerFeeBtc(),
+ buyerSecurityDepositAsCoin.value,
+ sellerSecurityDeposit.value,
+ maxTradeLimit,
+ maxTradePeriod,
+ useAutoClose,
+ useReOpenAfterAutoClose,
+ upperClosePrice,
+ lowerClosePrice,
+ isPrivateOffer,
+ hashOfChallenge,
+ extraDataMap,
+ Version.TRADE_PROTOCOL_VERSION);
+ Offer offer = new Offer(offerPayload);
+ offer.setPriceFeedService(priceFeedService);
+ return offer;
+ }
+
+ // This works only if have already funds in the wallet
+ public void estimateTxSize() {
+ txFeeFromFeeService = feeService.getTxFee(feeTxSize);
+ Address fundingAddress = btcWalletService.getOrCreateAddressEntry(AddressEntry.Context.AVAILABLE).getAddress();
+ Address reservedForTradeAddress = btcWalletService.getOrCreateAddressEntry(offerId, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress();
+ Address changeAddress = btcWalletService.getOrCreateAddressEntry(AddressEntry.Context.AVAILABLE).getAddress();
+
+ Coin reservedFundsForOffer = getSecurityDeposit();
+ if (!isBuyOffer())
+ reservedFundsForOffer = reservedFundsForOffer.add(amount.get());
+
+ checkNotNull(user.getAcceptedArbitrators(), "user.getAcceptedArbitrators() must not be null");
+ checkArgument(!user.getAcceptedArbitrators().isEmpty(), "user.getAcceptedArbitrators() must not be empty");
+ String dummyArbitratorAddress = user.getAcceptedArbitrators().get(0).getBtcAddress();
+ try {
+ log.info("We create a dummy tx to see if our estimated size is in the accepted range. feeTxSize={}," +
+ " txFee based on feeTxSize: {}, recommended txFee is {} sat/byte",
+ feeTxSize, txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte());
+ Transaction tradeFeeTx = tradeWalletService.estimateBtcTradingFeeTxSize(
+ fundingAddress,
+ reservedForTradeAddress,
+ changeAddress,
+ reservedFundsForOffer,
+ true,
+ getMakerFee(),
+ txFeeFromFeeService,
+ dummyArbitratorAddress);
+
+ final int txSize = tradeFeeTx.bitcoinSerialize().length;
+ // use feeTxSizeEstimationRecursionCounter to avoid risk for endless loop
+ if (txSize > feeTxSize * 1.2 && feeTxSizeEstimationRecursionCounter < 10) {
+ feeTxSizeEstimationRecursionCounter++;
+ log.info("txSize is {} bytes but feeTxSize used for txFee calculation was {} bytes. We try again with an " +
+ "adjusted txFee to reach the target tx fee.", txSize, feeTxSize);
+ feeTxSize = txSize;
+ txFeeFromFeeService = feeService.getTxFee(feeTxSize);
+ // lets try again with the adjusted txSize and fee.
+ estimateTxSize();
+ } else {
+ log.info("feeTxSize {} bytes", feeTxSize);
+ log.info("txFee based on estimated size: {}, recommended txFee is {} sat/byte",
+ txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte());
+ }
+ } catch (InsufficientMoneyException e) {
+ // If we need to fund from an external wallet we can assume we only have 1 input (260 bytes).
+ log.warn("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " +
+ "if the user pays from an external wallet. In that case we use an estimated tx size of 260 bytes.");
+ feeTxSize = 260;
+ txFeeFromFeeService = feeService.getTxFee(feeTxSize);
+ log.info("feeTxSize {} bytes", feeTxSize);
+ log.info("txFee based on estimated size: {}, recommended txFee is {} sat/byte",
+ txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte());
+ }
+ }
+
+ void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler) {
+ checkNotNull(getMakerFee(), "makerFee must not be null");
+
+ Coin reservedFundsForOffer = getSecurityDeposit();
+ if (!isBuyOffer())
+ reservedFundsForOffer = reservedFundsForOffer.add(amount.get());
+
+ openOfferManager.placeOffer(offer,
+ reservedFundsForOffer,
+ useSavingsWallet,
+ resultHandler,
+ log::error);
+ }
+
+ void onPaymentAccountSelected(PaymentAccount paymentAccount) {
+ if (paymentAccount != null && !this.paymentAccount.equals(paymentAccount)) {
+ volume.set(null);
+ price.set(null);
+ marketPriceMargin = 0;
+ preferences.setSelectedPaymentAccountForCreateOffer(paymentAccount);
+ this.paymentAccount = paymentAccount;
+
+ setTradeCurrencyFromPaymentAccount(paymentAccount);
+
+ long myLimit = accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get());
+ if (amount.get() != null)
+ this.amount.set(Coin.valueOf(Math.min(amount.get().value, myLimit)));
+ }
+ }
+
+ private void setTradeCurrencyFromPaymentAccount(PaymentAccount paymentAccount) {
+ if (paymentAccount.getSelectedTradeCurrency() != null)
+ tradeCurrency = paymentAccount.getSelectedTradeCurrency();
+ else if (paymentAccount.getSingleTradeCurrency() != null)
+ tradeCurrency = paymentAccount.getSingleTradeCurrency();
+ else if (!paymentAccount.getTradeCurrencies().isEmpty())
+ tradeCurrency = paymentAccount.getTradeCurrencies().get(0);
+
+ checkNotNull(tradeCurrency, "tradeCurrency must not be null");
+ tradeCurrencyCode.set(tradeCurrency.getCode());
+ }
+
+ void onCurrencySelected(TradeCurrency tradeCurrency) {
+ if (tradeCurrency != null) {
+ if (!this.tradeCurrency.equals(tradeCurrency)) {
+ volume.set(null);
+ price.set(null);
+ marketPriceMargin = 0;
+ }
+
+ this.tradeCurrency = tradeCurrency;
+ final String code = this.tradeCurrency.getCode();
+ tradeCurrencyCode.set(code);
+
+ if (paymentAccount != null)
+ paymentAccount.setSelectedTradeCurrency(tradeCurrency);
+
+ priceFeedService.setCurrencyCode(code);
+
+ Optional tradeCurrencyOptional = preferences.getTradeCurrenciesAsObservable().stream().filter(e -> e.getCode().equals(code)).findAny();
+ if (!tradeCurrencyOptional.isPresent()) {
+ if (CurrencyUtil.isCryptoCurrency(code)) {
+ CurrencyUtil.getCryptoCurrency(code).ifPresent(preferences::addCryptoCurrency);
+ } else {
+ CurrencyUtil.getFiatCurrency(code).ifPresent(preferences::addFiatCurrency);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onUpdateBalances(Coin confirmedBalance,
+ Coin pendingBalance,
+ Coin lockedForVotingBalance,
+ Coin lockedInBondsBalance) {
+ updateBalance();
+ }
+
+ void fundFromSavingsWallet() {
+ this.useSavingsWallet = true;
+ updateBalance();
+ if (!isBtcWalletFunded.get()) {
+ this.useSavingsWallet = false;
+ updateBalance();
+ }
+ }
+
+ protected void setMarketPriceMargin(double marketPriceMargin) {
+ this.marketPriceMargin = marketPriceMargin;
+ }
+
+ void requestTxFee() {
+ feeService.requestFees(() -> {
+ txFeeFromFeeService = feeService.getTxFee(feeTxSize);
+ calculateTotalToPay();
+ }, null);
+ }
+
+ void setPreferredCurrencyForMakerFeeBtc(boolean preferredCurrencyForMakerFeeBtc) {
+ preferences.setPayFeeInBtc(preferredCurrencyForMakerFeeBtc);
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Getters
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ boolean isMinAmountLessOrEqualAmount() {
+ //noinspection SimplifiableIfStatement
+ if (minAmount.get() != null && amount.get() != null)
+ return !minAmount.get().isGreaterThan(amount.get());
+ return true;
+ }
+
+ OfferPayload.Direction getDirection() {
+ return direction;
+ }
+
+ AddressEntry getAddressEntry() {
+ return addressEntry;
+ }
+
+ protected TradeCurrency getTradeCurrency() {
+ return tradeCurrency;
+ }
+
+ protected PaymentAccount getPaymentAccount() {
+ return paymentAccount;
+ }
+
+ boolean hasAcceptedArbitrators() {
+ final List acceptedArbitrators = user.getAcceptedArbitrators();
+ return acceptedArbitrators != null && acceptedArbitrators.size() > 0;
+ }
+
+ protected void setUseMarketBasedPrice(boolean useMarketBasedPrice) {
+ this.useMarketBasedPrice.set(useMarketBasedPrice);
+ preferences.setUsePercentageBasedPrice(useMarketBasedPrice);
+ }
+
+ /*boolean isFeeFromFundingTxSufficient() {
+ return !isMainNet.get() || feeFromFundingTxProperty.get().compareTo(FeePolicy.getMinRequiredFeeForFundingTx()) >= 0;
+ }*/
+
+ public ObservableList getPaymentAccounts() {
+ return paymentAccounts;
+ }
+
+ public double getMarketPriceMargin() {
+ return marketPriceMargin;
+ }
+
+ boolean isMakerFeeValid() {
+ return preferences.getPayFeeInBtc() || isBsqForFeeAvailable();
+ }
+
+ long getMaxTradeLimit() {
+ if (paymentAccount != null)
+ return accountAgeWitnessService.getMyTradeLimit(paymentAccount, tradeCurrencyCode.get());
+ else
+ return 0;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Utils
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ void calculateVolume() {
+ if (price.get() != null &&
+ amount.get() != null &&
+ !amount.get().isZero() &&
+ !price.get().isZero()) {
+ try {
+ volume.set(price.get().getVolumeByAmount(amount.get()));
+ } catch (Throwable t) {
+ log.error(t.toString());
+ }
+ }
+
+ updateBalance();
+ }
+
+ void calculateAmount() {
+ if (volume.get() != null &&
+ price.get() != null &&
+ !volume.get().isZero() &&
+ !price.get().isZero()) {
+ try {
+ amount.set(formatter.reduceTo4Decimals(price.get().getAmountByVolume(volume.get())));
+ calculateTotalToPay();
+ } catch (Throwable t) {
+ log.error(t.toString());
+ }
+ }
+ }
+
+ void calculateTotalToPay() {
+ // Maker does not pay the tx fee for the trade txs because the mining fee might be different when maker
+ // created the offer and reserved his funds, so that would not work well with dynamic fees.
+ // The mining fee for the createOfferFee tx is deducted from the createOfferFee and not visible to the trader
+ final Coin makerFee = getMakerFee();
+ if (direction != null && amount.get() != null && makerFee != null) {
+ Coin feeAndSecDeposit = getTxFee().add(getSecurityDeposit());
+ if (isCurrencyForMakerFeeBtc())
+ feeAndSecDeposit = feeAndSecDeposit.add(makerFee);
+ Coin total = isBuyOffer() ? feeAndSecDeposit : feeAndSecDeposit.add(amount.get());
+ totalToPayAsCoin.set(total);
+ updateBalance();
+ }
+ }
+
+ Coin getSecurityDeposit() {
+ return isBuyOffer() ? buyerSecurityDeposit.get() : sellerSecurityDeposit;
+ }
+
+ public boolean isBuyOffer() {
+ return OfferUtil.isBuyOffer(getDirection());
+ }
+
+ public Coin getTxFee() {
+ if (isCurrencyForMakerFeeBtc())
+ return txFeeFromFeeService;
+ else
+ return txFeeFromFeeService.subtract(getMakerFee());
+ }
+
+ public void swapTradeToSavings() {
+ btcWalletService.resetAddressEntriesForOpenOffer(offerId);
+ }
+
+ private void fillPaymentAccounts() {
+ if (user.getPaymentAccounts() != null)
+ paymentAccounts.setAll(new HashSet<>(user.getPaymentAccounts()));
+ }
+
+ protected void setAmount(Coin amount) {
+ this.amount.set(amount);
+ }
+
+ protected void setPrice(Price price) {
+ this.price.set(price);
+ }
+
+ protected void setVolume(Volume volume) {
+ this.volume.set(volume);
+ }
+
+ void setBuyerSecurityDeposit(Coin buyerSecurityDeposit) {
+ this.buyerSecurityDeposit.set(buyerSecurityDeposit);
+ preferences.setBuyerSecurityDepositAsLong(buyerSecurityDeposit.value);
+ }
+
+ protected boolean isUseMarketBasedPriceValue() {
+ return marketPriceAvailable && useMarketBasedPrice.get();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Getters
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ protected ReadOnlyObjectProperty getAmount() {
+ return amount;
+ }
+
+ protected ReadOnlyObjectProperty getMinAmount() {
+ return minAmount;
+ }
+
+ public ReadOnlyObjectProperty getPrice() {
+ return price;
+ }
+
+ ReadOnlyObjectProperty getVolume() {
+ return volume;
+ }
+
+ protected void setMinAmount(Coin minAmount) {
+ this.minAmount.set(minAmount);
+ }
+
+ ReadOnlyStringProperty getTradeCurrencyCode() {
+ return tradeCurrencyCode;
+ }
+
+ ReadOnlyBooleanProperty getUseMarketBasedPrice() {
+ return useMarketBasedPrice;
+ }
+
+ ReadOnlyObjectProperty getBuyerSecurityDeposit() {
+ return buyerSecurityDeposit;
+ }
+
+ Coin getSellerSecurityDeposit() {
+ return sellerSecurityDeposit;
+ }
+
+ ReadOnlyObjectProperty totalToPayAsCoinProperty() {
+ return totalToPayAsCoin;
+ }
+
+ public Coin getBsqBalance() {
+ return bsqWalletService.getAvailableBalance();
+ }
+
+ public void setMarketPriceAvailable(boolean marketPriceAvailable) {
+ this.marketPriceAvailable = marketPriceAvailable;
+ }
+
+ public Coin getMakerFee(boolean isCurrencyForMakerFeeBtc) {
+ return OfferUtil.getMakerFee(isCurrencyForMakerFeeBtc, amount.get(), marketPriceAvailable, marketPriceMargin);
+ }
+
+ public Coin getMakerFee() {
+ return OfferUtil.getMakerFee(bsqWalletService, preferences, amount.get(), marketPriceAvailable, marketPriceMargin);
+ }
+
+ public boolean isCurrencyForMakerFeeBtc() {
+ return OfferUtil.isCurrencyForMakerFeeBtc(preferences, bsqWalletService, amount.get(), marketPriceAvailable, marketPriceMargin);
+ }
+
+ public boolean isBsqForFeeAvailable() {
+ return OfferUtil.isBsqForFeeAvailable(bsqWalletService, amount.get(), marketPriceAvailable, marketPriceMargin);
+ }
+}
diff --git a/src/main/java/bisq/desktop/main/offer/EditableOfferView.java b/src/main/java/bisq/desktop/main/offer/EditableOfferView.java
new file mode 100644
index 00000000000..6eef60ba6ea
--- /dev/null
+++ b/src/main/java/bisq/desktop/main/offer/EditableOfferView.java
@@ -0,0 +1,1322 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.desktop.main.offer;
+
+import bisq.desktop.Navigation;
+import bisq.desktop.common.view.ActivatableViewAndModel;
+import bisq.desktop.components.AddressTextField;
+import bisq.desktop.components.AutoTooltipButton;
+import bisq.desktop.components.AutoTooltipLabel;
+import bisq.desktop.components.BalanceTextField;
+import bisq.desktop.components.BusyAnimation;
+import bisq.desktop.components.FundsTextField;
+import bisq.desktop.components.InfoInputTextField;
+import bisq.desktop.components.InputTextField;
+import bisq.desktop.components.TitledGroupBg;
+import bisq.desktop.main.MainView;
+import bisq.desktop.main.account.AccountView;
+import bisq.desktop.main.account.content.arbitratorselection.ArbitratorSelectionView;
+import bisq.desktop.main.account.content.fiataccounts.FiatAccountsView;
+import bisq.desktop.main.account.settings.AccountSettingsView;
+import bisq.desktop.main.dao.DaoView;
+import bisq.desktop.main.dao.wallet.BsqWalletView;
+import bisq.desktop.main.dao.wallet.receive.BsqReceiveView;
+import bisq.desktop.main.funds.FundsView;
+import bisq.desktop.main.funds.withdrawal.WithdrawalView;
+import bisq.desktop.main.overlays.notifications.Notification;
+import bisq.desktop.main.overlays.popups.Popup;
+import bisq.desktop.main.overlays.windows.FeeOptionWindow;
+import bisq.desktop.main.overlays.windows.OfferDetailsWindow;
+import bisq.desktop.main.overlays.windows.QRCodeWindow;
+import bisq.desktop.main.portfolio.PortfolioView;
+import bisq.desktop.main.portfolio.openoffer.OpenOffersView;
+import bisq.desktop.util.BSFormatter;
+import bisq.desktop.util.BsqFormatter;
+import bisq.desktop.util.GUIUtil;
+import bisq.desktop.util.Layout;
+import bisq.desktop.util.Transitions;
+
+import bisq.core.locale.Res;
+import bisq.core.locale.TradeCurrency;
+import bisq.core.offer.Offer;
+import bisq.core.offer.OfferPayload;
+import bisq.core.payment.PaymentAccount;
+import bisq.core.payment.payload.PaymentMethod;
+import bisq.core.user.DontShowAgainLookup;
+import bisq.core.user.Preferences;
+
+import bisq.common.UserThread;
+import bisq.common.app.DevEnv;
+import bisq.common.util.Tuple2;
+import bisq.common.util.Tuple3;
+import bisq.common.util.Utilities;
+
+import org.bitcoinj.core.Coin;
+
+import net.glxn.qrgen.QRCode;
+import net.glxn.qrgen.image.ImageType;
+
+import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
+
+import javafx.scene.Node;
+import javafx.scene.control.Button;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.Separator;
+import javafx.scene.control.TextField;
+import javafx.scene.control.Tooltip;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.ColumnConstraints;
+import javafx.scene.layout.GridPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Font;
+
+import javafx.geometry.HPos;
+import javafx.geometry.Insets;
+import javafx.geometry.Orientation;
+import javafx.geometry.Pos;
+import javafx.geometry.VPos;
+
+import org.fxmisc.easybind.EasyBind;
+import org.fxmisc.easybind.Subscription;
+
+import javafx.beans.value.ChangeListener;
+
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
+
+import javafx.collections.FXCollections;
+
+import javafx.util.StringConverter;
+
+import java.net.URI;
+
+import java.io.ByteArrayInputStream;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import org.jetbrains.annotations.NotNull;
+
+import static bisq.desktop.util.FormBuilder.*;
+import static javafx.beans.binding.Bindings.createStringBinding;
+
+public abstract class EditableOfferView extends ActivatableViewAndModel {
+ protected final Navigation navigation;
+ private final Preferences preferences;
+ private final Transitions transitions;
+ private final OfferDetailsWindow offerDetailsWindow;
+ private final BSFormatter btcFormatter;
+ private final BsqFormatter bsqFormatter;
+
+ private ScrollPane scrollPane;
+ protected GridPane gridPane;
+ private TitledGroupBg payFundsTitledGroupBg, setDepositTitledGroupBg, paymentTitledGroupBg;
+ private BusyAnimation waitingForFundsBusyAnimation;
+ private Button nextButton, cancelButton1, cancelButton2, placeOfferButton, priceTypeToggleButton;
+ private InputTextField buyerSecurityDepositInputTextField, amountTextField, minAmountTextField,
+ fixedPriceTextField, marketBasedPriceTextField, volumeTextField;
+ private TextField currencyTextField;
+ private AddressTextField addressTextField;
+ private BalanceTextField balanceTextField;
+ private FundsTextField totalToPayTextField;
+ private Label directionLabel, amountDescriptionLabel, addressLabel, balanceLabel, totalToPayLabel,
+ priceCurrencyLabel, volumeCurrencyLabel, priceDescriptionLabel,
+ volumeDescriptionLabel, currencyTextFieldLabel, buyerSecurityDepositLabel, currencyComboBoxLabel,
+ waitingForFundsLabel, marketBasedPriceLabel, xLabel, percentagePriceDescription, resultLabel,
+ buyerSecurityDepositBtcLabel, paymentAccountsLabel;
+ private ComboBox paymentAccountsComboBox;
+ private ComboBox currencyComboBox;
+ private ImageView imageView, qrCodeImageView;
+ private VBox fixedPriceBox, percentagePriceBox;
+ private HBox fundingHBox, firstRowHBox, secondRowHBox, buyerSecurityDepositValueCurrencyBox;
+
+ private Subscription isWaitingForFundsSubscription, balanceSubscription, cancelButton2StyleSubscription;
+ private ChangeListener amountFocusedListener, minAmountFocusedListener, volumeFocusedListener,
+ buyerSecurityDepositFocusedListener, priceFocusedListener, placeOfferCompletedListener,
+ priceAsPercentageFocusedListener;
+ private ChangeListener tradeCurrencyCodeListener, errorMessageListener, marketPriceMarginListener;
+ private ChangeListener marketPriceAvailableListener;
+ private EventHandler currencyComboBoxSelectionHandler, paymentAccountsComboBoxSelectionHandler;
+ private OfferView.CloseHandler closeHandler;
+
+ protected int gridRow = 0;
+ private final List editOfferElements = new ArrayList<>();
+ private boolean clearXchangeWarningDisplayed, isActivated;
+ private ChangeListener getShowWalletFundedNotificationListener;
+ private InfoInputTextField marketBasedPriceInfoInputTextField;
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Constructor, lifecycle
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ public EditableOfferView(M model, Navigation navigation, Preferences preferences, Transitions transitions,
+ OfferDetailsWindow offerDetailsWindow, BSFormatter btcFormatter, BsqFormatter bsqFormatter) {
+ super(model);
+
+ this.navigation = navigation;
+ this.preferences = preferences;
+ this.transitions = transitions;
+ this.offerDetailsWindow = offerDetailsWindow;
+ this.btcFormatter = btcFormatter;
+ this.bsqFormatter = bsqFormatter;
+ }
+
+ @Override
+ protected void initialize() {
+ addScrollPane();
+ addGridPane();
+ addPaymentGroup();
+ addAmountPriceGroup();
+ addOptionsGroup();
+ addFundingGroup();
+
+ createListeners();
+
+ balanceTextField.setFormatter(model.getBtcFormatter());
+
+ paymentAccountsComboBox.setConverter(GUIUtil.getPaymentAccountsComboBoxStringConverter());
+
+ GUIUtil.focusWhenAddedToScene(amountTextField);
+ }
+
+ @Override
+ protected void activate() {
+ if (model.getDataModel().isTabSelected)
+ doActivate();
+ }
+
+ protected void doActivate() {
+ if (!isActivated) {
+ isActivated = true;
+ currencyComboBox.setPrefWidth(250);
+ paymentAccountsComboBox.setPrefWidth(250);
+
+ addBindings();
+ addListeners();
+ addSubscriptions();
+
+ if (waitingForFundsBusyAnimation != null)
+ waitingForFundsBusyAnimation.play();
+
+ directionLabel.setText(model.getDirectionLabel());
+ amountDescriptionLabel.setText(model.getAmountDescription());
+ addressTextField.setAddress(model.getAddressAsString());
+ addressTextField.setPaymentLabel(model.getPaymentLabel());
+
+ paymentAccountsComboBox.setItems(model.getDataModel().getPaymentAccounts());
+ paymentAccountsComboBox.getSelectionModel().select(model.getPaymentAccount());
+
+ onPaymentAccountsComboBoxSelected();
+
+ balanceTextField.setTargetAmount(model.getDataModel().totalToPayAsCoinProperty().get());
+ updateMarketPriceAvailable();
+ }
+ }
+
+ @Override
+ protected void deactivate() {
+ if (isActivated) {
+ isActivated = false;
+ removeBindings();
+ removeListeners();
+ removeSubscriptions();
+
+ if (waitingForFundsBusyAnimation != null)
+ waitingForFundsBusyAnimation.stop();
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // API
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ public void onTabSelected(boolean isSelected) {
+ if (isSelected && !model.getDataModel().isTabSelected)
+ doActivate();
+ else
+ deactivate();
+
+ isActivated = isSelected;
+ model.getDataModel().onTabSelected(isSelected);
+ }
+
+ public void initWithData(OfferPayload.Direction direction, TradeCurrency tradeCurrency) {
+ boolean result = model.initWithData(direction, tradeCurrency);
+
+ if (!result) {
+ new Popup<>().headLine(Res.get("popup.warning.noTradingAccountSetup.headline"))
+ .instruction(Res.get("popup.warning.noTradingAccountSetup.msg"))
+ .actionButtonTextWithGoTo("navigation.account")
+ .onAction(() -> {
+ navigation.setReturnPath(navigation.getCurrentPath());
+ //noinspection unchecked
+ navigation.navigateTo(MainView.class, AccountView.class, AccountSettingsView.class, FiatAccountsView.class);
+ }).show();
+ }
+
+ if (direction == OfferPayload.Direction.BUY) {
+ imageView.setId("image-buy-large");
+
+ placeOfferButton.setId("buy-button-big");
+ placeOfferButton.setText(Res.get("createOffer.placeOfferButton", Res.get("shared.buy")));
+ nextButton.setId("buy-button");
+ percentagePriceDescription.setText(Res.get("shared.belowInPercent"));
+ } else {
+ imageView.setId("image-sell-large");
+ placeOfferButton.setId("sell-button-big");
+ placeOfferButton.setText(Res.get("createOffer.placeOfferButton", Res.get("shared.sell")));
+ nextButton.setId("sell-button");
+ percentagePriceDescription.setText(Res.get("shared.aboveInPercent"));
+ }
+
+ updateMarketPriceAvailable();
+
+ if (!model.getDataModel().isMakerFeeValid() && model.getDataModel().getMakerFee() != null)
+ showInsufficientBsqFundsForBtcFeePaymentPopup();
+ }
+
+ // called form parent as the view does not get notified when the tab is closed
+ public void onClose() {
+ // we use model.placeOfferCompleted to not react on close which was triggered by a successful placeOffer
+ if (model.getDataModel().getBalance().get().isPositive() && !model.placeOfferCompleted.get()) {
+ model.getDataModel().swapTradeToSavings();
+ String key = "CreateOfferCancelAndFunded";
+ if (preferences.showAgain(key)) {
+ //noinspection unchecked
+ new Popup<>().information(Res.get("createOffer.alreadyFunded"))
+ .actionButtonTextWithGoTo("navigation.funds.availableForWithdrawal")
+ .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class))
+ .dontShowAgainId(key)
+ .show();
+ }
+ }
+ }
+
+ public void setCloseHandler(OfferView.CloseHandler closeHandler) {
+ this.closeHandler = closeHandler;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // UI actions
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ private void onPlaceOffer() {
+ if (model.isReadyForTxBroadcast()) {
+ if (model.getDataModel().isMakerFeeValid()) {
+ if (model.hasAcceptedArbitrators()) {
+ Offer offer = model.createAndGetOffer();
+ //noinspection PointlessBooleanExpression
+ if (!DevEnv.isDevMode()) {
+ offerDetailsWindow.onPlaceOffer(() ->
+ model.onPlaceOffer(offer, offerDetailsWindow::hide))
+ .show(offer);
+ } else {
+ balanceSubscription.unsubscribe();
+ model.onPlaceOffer(offer, () -> {
+ });
+ }
+ } else {
+ new Popup<>().headLine(Res.get("popup.warning.noArbitratorSelected.headline"))
+ .instruction(Res.get("popup.warning.noArbitratorSelected.msg"))
+ .actionButtonTextWithGoTo("navigation.arbitratorSelection")
+ .onAction(() -> {
+ navigation.setReturnPath(navigation.getCurrentPath());
+ //noinspection unchecked
+ navigation.navigateTo(MainView.class, AccountView.class, AccountSettingsView.class, ArbitratorSelectionView.class);
+ }).show();
+ }
+ } else {
+ showInsufficientBsqFundsForBtcFeePaymentPopup();
+ }
+ } else {
+ model.showNotReadyForTxBroadcastPopups();
+ }
+ }
+
+ private void showInsufficientBsqFundsForBtcFeePaymentPopup() {
+ Coin makerFee = model.getDataModel().getMakerFee(false);
+ String message = null;
+ if (makerFee != null) {
+ message = Res.get("popup.warning.insufficientBsqFundsForBtcFeePayment",
+ bsqFormatter.formatCoinWithCode(makerFee.subtract(model.getDataModel().getBsqBalance())));
+
+ } else if (model.getDataModel().getBsqBalance().isZero())
+ message = Res.get("popup.warning.noBsqFundsForBtcFeePayment");
+
+ if (message != null)
+ //noinspection unchecked
+ new Popup<>().warning(message)
+ .actionButtonTextWithGoTo("navigation.dao.wallet.receive")
+ .onAction(() -> navigation.navigateTo(MainView.class, DaoView.class, BsqWalletView.class, BsqReceiveView.class))
+ .show();
+ }
+
+ private void onShowPayFundsScreen() {
+ nextButton.setVisible(false);
+ nextButton.setManaged(false);
+ nextButton.setOnAction(null);
+ cancelButton1.setVisible(false);
+ cancelButton1.setManaged(false);
+ cancelButton1.setOnAction(null);
+
+ int delay = 500;
+ int diff = 100;
+
+ transitions.fadeOutAndRemove(setDepositTitledGroupBg, delay, (event) -> {
+ });
+ delay -= diff;
+ transitions.fadeOutAndRemove(buyerSecurityDepositLabel, delay);
+ transitions.fadeOutAndRemove(buyerSecurityDepositValueCurrencyBox, delay);
+
+ model.onShowPayFundsScreen();
+
+ editOfferElements.stream().forEach(node -> {
+ node.setMouseTransparent(true);
+ node.setFocusTraversable(false);
+ });
+
+ balanceTextField.setTargetAmount(model.getDataModel().totalToPayAsCoinProperty().get());
+
+ //noinspection PointlessBooleanExpression
+ if (!DevEnv.isDevMode()) {
+ String key = "securityDepositInfo";
+ new Popup<>().backgroundInfo(Res.get("popup.info.securityDepositInfo"))
+ .actionButtonText(Res.get("shared.faq"))
+ .onAction(() -> GUIUtil.openWebPage("https://bisq.network/faq#6"))
+ .useIUnderstandButton()
+ .dontShowAgainId(key)
+ .show();
+
+ key = "createOfferFundWalletInfo";
+ String tradeAmountText = model.isSellOffer() ?
+ Res.get("createOffer.createOfferFundWalletInfo.tradeAmount", model.getTradeAmount()) : "";
+ String message = Res.get("createOffer.createOfferFundWalletInfo.msg",
+ model.totalToPay.get(),
+ tradeAmountText,
+ model.getSecurityDepositInfo(),
+ model.getMakerFee(),
+ model.getTxFee()
+ );
+ new Popup<>().headLine(Res.get("createOffer.createOfferFundWalletInfo.headline"))
+ .instruction(message)
+ .dontShowAgainId(key)
+ .show();
+ }
+
+ waitingForFundsBusyAnimation.play();
+
+ payFundsTitledGroupBg.setVisible(true);
+ totalToPayLabel.setVisible(true);
+ totalToPayTextField.setVisible(true);
+ addressLabel.setVisible(true);
+ addressTextField.setVisible(true);
+ qrCodeImageView.setVisible(true);
+ balanceLabel.setVisible(true);
+ balanceTextField.setVisible(true);
+ cancelButton2.setVisible(true);
+
+ totalToPayTextField.setFundsStructure(Res.get("createOffer.fundsBox.fundsStructure",
+ model.getSecurityDepositWithCode(), model.getMakerFeePercentage(), model.getTxFeePercentage()));
+ totalToPayTextField.setContentForInfoPopOver(createInfoPopover());
+
+ final byte[] imageBytes = QRCode
+ .from(getBitcoinURI())
+ .withSize(98, 98) // code has 41 elements 8 px is border with 98 we get double scale and min. border
+ .to(ImageType.PNG)
+ .stream()
+ .toByteArray();
+ Image qrImage = new Image(new ByteArrayInputStream(imageBytes));
+ qrCodeImageView.setImage(qrImage);
+ }
+
+ private void maybeShowClearXchangeWarning(PaymentAccount paymentAccount) {
+ if (paymentAccount.getPaymentMethod().getId().equals(PaymentMethod.CLEAR_X_CHANGE_ID) &&
+ !clearXchangeWarningDisplayed) {
+ clearXchangeWarningDisplayed = true;
+ UserThread.runAfter(GUIUtil::showClearXchangeWarning,
+ 500, TimeUnit.MILLISECONDS);
+ }
+ }
+
+ private void onPaymentAccountsComboBoxSelected() {
+ // Temporary deactivate handler as the payment account change can populate a new currency list and causes
+ // unwanted selection events (item 0)
+ currencyComboBox.setOnAction(null);
+
+ PaymentAccount paymentAccount = paymentAccountsComboBox.getSelectionModel().getSelectedItem();
+ if (paymentAccount != null) {
+ maybeShowClearXchangeWarning(paymentAccount);
+
+ currencyComboBox.setVisible(paymentAccount.hasMultipleCurrencies());
+ currencyTextField.setVisible(!paymentAccount.hasMultipleCurrencies());
+ currencyTextFieldLabel.setVisible(!paymentAccount.hasMultipleCurrencies());
+ if (paymentAccount.hasMultipleCurrencies()) {
+ final List tradeCurrencies = paymentAccount.getTradeCurrencies();
+ currencyComboBox.setItems(FXCollections.observableArrayList(tradeCurrencies));
+ if (paymentAccount.getSelectedTradeCurrency() != null)
+ currencyComboBox.getSelectionModel().select(paymentAccount.getSelectedTradeCurrency());
+ else if (tradeCurrencies.contains(model.getTradeCurrency()))
+ currencyComboBox.getSelectionModel().select(model.getTradeCurrency());
+ else
+ currencyComboBox.getSelectionModel().select(tradeCurrencies.get(0));
+
+ model.onPaymentAccountSelected(paymentAccount);
+ } else {
+ TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency();
+ if (singleTradeCurrency != null)
+ currencyTextField.setText(singleTradeCurrency.getNameAndCode());
+ model.onPaymentAccountSelected(paymentAccount);
+ model.onCurrencySelected(model.getDataModel().getTradeCurrency());
+ }
+ } else {
+ currencyComboBox.setVisible(false);
+ currencyTextField.setVisible(true);
+ currencyTextFieldLabel.setVisible(true);
+
+ currencyTextField.setText("");
+ }
+
+ currencyComboBox.setOnAction(currencyComboBoxSelectionHandler);
+ }
+
+ private void onCurrencyComboBoxSelected() {
+ model.onCurrencySelected(currencyComboBox.getSelectionModel().getSelectedItem());
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Navigation
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ protected void close() {
+ if (closeHandler != null)
+ closeHandler.close();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Bindings, Listeners
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ private void addBindings() {
+ priceCurrencyLabel.textProperty().bind(createStringBinding(() -> btcFormatter.getCounterCurrency(model.tradeCurrencyCode.get()), model.tradeCurrencyCode));
+
+ marketBasedPriceLabel.prefWidthProperty().bind(priceCurrencyLabel.widthProperty());
+ volumeCurrencyLabel.textProperty().bind(model.tradeCurrencyCode);
+ priceDescriptionLabel.textProperty().bind(createStringBinding(() -> btcFormatter.getPriceWithCurrencyCode(model.tradeCurrencyCode.get(), "shared.fixedPriceInCurForCur"), model.tradeCurrencyCode));
+ xLabel.setText("x");
+ volumeDescriptionLabel.textProperty().bind(createStringBinding(model.volumeDescriptionLabel::get, model.tradeCurrencyCode, model.volumeDescriptionLabel));
+ amountTextField.textProperty().bindBidirectional(model.amount);
+ minAmountTextField.textProperty().bindBidirectional(model.minAmount);
+ fixedPriceTextField.textProperty().bindBidirectional(model.price);
+ marketBasedPriceTextField.textProperty().bindBidirectional(model.marketPriceMargin);
+ volumeTextField.textProperty().bindBidirectional(model.volume);
+ volumeTextField.promptTextProperty().bind(model.volumePromptLabel);
+ totalToPayTextField.textProperty().bind(model.totalToPay);
+ addressTextField.amountAsCoinProperty().bind(model.getDataModel().getMissingCoin());
+ buyerSecurityDepositInputTextField.textProperty().bindBidirectional(model.buyerSecurityDeposit);
+
+ // Validation
+ amountTextField.validationResultProperty().bind(model.amountValidationResult);
+ minAmountTextField.validationResultProperty().bind(model.minAmountValidationResult);
+ fixedPriceTextField.validationResultProperty().bind(model.priceValidationResult);
+ volumeTextField.validationResultProperty().bind(model.volumeValidationResult);
+ buyerSecurityDepositInputTextField.validationResultProperty().bind(model.buyerSecurityDepositValidationResult);
+
+ // funding
+ fundingHBox.visibleProperty().bind(model.getDataModel().getIsBtcWalletFunded().not().and(model.showPayFundsScreenDisplayed));
+ fundingHBox.managedProperty().bind(model.getDataModel().getIsBtcWalletFunded().not().and(model.showPayFundsScreenDisplayed));
+ waitingForFundsLabel.textProperty().bind(model.waitingForFundsText);
+ placeOfferButton.visibleProperty().bind(model.getDataModel().getIsBtcWalletFunded().and(model.showPayFundsScreenDisplayed));
+ placeOfferButton.managedProperty().bind(model.getDataModel().getIsBtcWalletFunded().and(model.showPayFundsScreenDisplayed));
+ placeOfferButton.disableProperty().bind(model.isPlaceOfferButtonDisabled);
+ cancelButton2.disableProperty().bind(model.cancelButtonDisabled);
+
+ // trading account
+ paymentAccountsComboBox.managedProperty().bind(paymentAccountsComboBox.visibleProperty());
+ paymentAccountsLabel.managedProperty().bind(paymentAccountsLabel.visibleProperty());
+ paymentTitledGroupBg.managedProperty().bind(paymentTitledGroupBg.visibleProperty());
+ currencyComboBox.prefWidthProperty().bind(paymentAccountsComboBox.widthProperty());
+ currencyComboBox.managedProperty().bind(currencyComboBox.visibleProperty());
+ currencyComboBoxLabel.visibleProperty().bind(currencyComboBox.visibleProperty());
+ currencyComboBoxLabel.managedProperty().bind(currencyComboBoxLabel.visibleProperty());
+ currencyTextField.managedProperty().bind(currencyTextField.visibleProperty());
+ currencyTextFieldLabel.managedProperty().bind(currencyTextFieldLabel.visibleProperty());
+ }
+
+ private void removeBindings() {
+ priceCurrencyLabel.textProperty().unbind();
+ fixedPriceTextField.disableProperty().unbind();
+ priceCurrencyLabel.disableProperty().unbind();
+ marketBasedPriceTextField.disableProperty().unbind();
+ marketBasedPriceLabel.disableProperty().unbind();
+ volumeCurrencyLabel.textProperty().unbind();
+ priceDescriptionLabel.textProperty().unbind();
+ xLabel.textProperty().unbind();
+ volumeDescriptionLabel.textProperty().unbind();
+ amountTextField.textProperty().unbindBidirectional(model.amount);
+ minAmountTextField.textProperty().unbindBidirectional(model.minAmount);
+ fixedPriceTextField.textProperty().unbindBidirectional(model.price);
+ marketBasedPriceTextField.textProperty().unbindBidirectional(model.marketPriceMargin);
+ marketBasedPriceLabel.prefWidthProperty().unbind();
+ volumeTextField.textProperty().unbindBidirectional(model.volume);
+ volumeTextField.promptTextProperty().unbindBidirectional(model.volume);
+ totalToPayTextField.textProperty().unbind();
+ addressTextField.amountAsCoinProperty().unbind();
+ buyerSecurityDepositInputTextField.textProperty().unbindBidirectional(model.buyerSecurityDeposit);
+
+ // Validation
+ amountTextField.validationResultProperty().unbind();
+ minAmountTextField.validationResultProperty().unbind();
+ fixedPriceTextField.validationResultProperty().unbind();
+ volumeTextField.validationResultProperty().unbind();
+ buyerSecurityDepositInputTextField.validationResultProperty().unbind();
+
+ // funding
+ fundingHBox.visibleProperty().unbind();
+ fundingHBox.managedProperty().unbind();
+ waitingForFundsLabel.textProperty().unbind();
+ placeOfferButton.visibleProperty().unbind();
+ placeOfferButton.managedProperty().unbind();
+ placeOfferButton.disableProperty().unbind();
+ cancelButton2.disableProperty().unbind();
+
+ // trading account
+ paymentTitledGroupBg.managedProperty().unbind();
+ paymentAccountsLabel.managedProperty().unbind();
+ paymentAccountsComboBox.managedProperty().unbind();
+ currencyComboBox.managedProperty().unbind();
+ currencyComboBox.prefWidthProperty().unbind();
+ currencyComboBoxLabel.visibleProperty().unbind();
+ currencyComboBoxLabel.managedProperty().unbind();
+ currencyTextField.managedProperty().unbind();
+ currencyTextFieldLabel.managedProperty().unbind();
+ }
+
+ private void addSubscriptions() {
+ isWaitingForFundsSubscription = EasyBind.subscribe(model.isWaitingForFunds, isWaitingForFunds -> {
+ waitingForFundsBusyAnimation.setIsRunning(isWaitingForFunds);
+ waitingForFundsLabel.setVisible(isWaitingForFunds);
+ waitingForFundsLabel.setManaged(isWaitingForFunds);
+ });
+
+ cancelButton2StyleSubscription = EasyBind.subscribe(placeOfferButton.visibleProperty(),
+ isVisible -> cancelButton2.setId(isVisible ? "cancel-button" : null));
+
+ balanceSubscription = EasyBind.subscribe(model.getDataModel().getBalance(), balanceTextField::setBalance);
+ }
+
+ private void removeSubscriptions() {
+ isWaitingForFundsSubscription.unsubscribe();
+ cancelButton2StyleSubscription.unsubscribe();
+ balanceSubscription.unsubscribe();
+ }
+
+ private void createListeners() {
+ amountFocusedListener = (o, oldValue, newValue) -> {
+ model.onFocusOutAmountTextField(oldValue, newValue);
+ amountTextField.setText(model.amount.get());
+ };
+ minAmountFocusedListener = (o, oldValue, newValue) -> {
+ model.onFocusOutMinAmountTextField(oldValue, newValue);
+ minAmountTextField.setText(model.minAmount.get());
+ };
+ priceFocusedListener = (o, oldValue, newValue) -> {
+ model.onFocusOutPriceTextField(oldValue, newValue);
+ fixedPriceTextField.setText(model.price.get());
+ };
+ priceAsPercentageFocusedListener = (o, oldValue, newValue) -> {
+ model.onFocusOutPriceAsPercentageTextField(oldValue, newValue);
+ marketBasedPriceTextField.setText(model.marketPriceMargin.get());
+ };
+ volumeFocusedListener = (o, oldValue, newValue) -> {
+ model.onFocusOutVolumeTextField(oldValue, newValue);
+ volumeTextField.setText(model.volume.get());
+ };
+ buyerSecurityDepositFocusedListener = (o, oldValue, newValue) -> {
+ model.onFocusOutBuyerSecurityDepositTextField(oldValue, newValue);
+ buyerSecurityDepositInputTextField.setText(model.buyerSecurityDeposit.get());
+ };
+
+ errorMessageListener = (o, oldValue, newValue) -> {
+ if (newValue != null)
+ UserThread.runAfter(() -> new Popup<>().error(Res.get("createOffer.amountPriceBox.error.message", model.errorMessage.get()))
+ .show(), 100, TimeUnit.MILLISECONDS);
+ };
+
+ paymentAccountsComboBoxSelectionHandler = e -> onPaymentAccountsComboBoxSelected();
+ currencyComboBoxSelectionHandler = e -> onCurrencyComboBoxSelected();
+
+ tradeCurrencyCodeListener = (observable, oldValue, newValue) -> {
+ fixedPriceTextField.clear();
+ marketBasedPriceTextField.clear();
+ volumeTextField.clear();
+ };
+
+ placeOfferCompletedListener = (o, oldValue, newValue) -> {
+ if (DevEnv.isDevMode()) {
+ close();
+ } else if (newValue) {
+ // We need a bit of delay to avoid issues with fade out/fade in of 2 popups
+ String key = "createOfferSuccessInfo";
+ if (DontShowAgainLookup.showAgain(key)) {
+ UserThread.runAfter(() -> new Popup<>().headLine(Res.get("createOffer.success.headline"))
+ .feedback(Res.get("createOffer.success.info"))
+ .dontShowAgainId(key)
+ .actionButtonTextWithGoTo("navigation.portfolio.myOpenOffers")
+ .onAction(() -> {
+ //noinspection unchecked
+ UserThread.runAfter(() ->
+ navigation.navigateTo(MainView.class, PortfolioView.class,
+ OpenOffersView.class),
+ 100, TimeUnit.MILLISECONDS);
+ close();
+ })
+ .onClose(this::close)
+ .show(),
+ 1);
+ } else {
+ close();
+ }
+ }
+ };
+
+ marketPriceAvailableListener = (observable, oldValue, newValue) -> updateMarketPriceAvailable();
+
+ getShowWalletFundedNotificationListener = (observable, oldValue, newValue) -> {
+ if (newValue) {
+ Notification walletFundedNotification = new Notification()
+ .headLine(Res.get("notification.walletUpdate.headline"))
+ .notification(Res.get("notification.walletUpdate.msg", btcFormatter.formatCoinWithCode(model.getDataModel().getTotalToPayAsCoin().get())))
+ .autoClose();
+
+ walletFundedNotification.show();
+ }
+ };
+
+ marketPriceMarginListener = (observable, oldValue, newValue) -> {
+ if (marketBasedPriceInfoInputTextField != null) {
+ String tooltip;
+ if (newValue.equals("0.00")) {
+ if (model.isSellOffer()) {
+ tooltip = Res.get("createOffer.info.sellAtMarketPrice");
+ } else {
+ tooltip = Res.get("createOffer.info.buyAtMarketPrice");
+ }
+ final Label atMarketPriceLabel = createPopoverLabel(tooltip);
+ marketBasedPriceInfoInputTextField.setContentForInfoPopOver(atMarketPriceLabel);
+ } else if (newValue.contains("-")) {
+ if (model.isSellOffer()) {
+ tooltip = Res.get("createOffer.warning.sellBelowMarketPrice", newValue.substring(1));
+ } else {
+ tooltip = Res.get("createOffer.warning.buyAboveMarketPrice", newValue.substring(1));
+ }
+ final Label negativePercentageLabel = createPopoverLabel(tooltip);
+ marketBasedPriceInfoInputTextField.setContentForWarningPopOver(negativePercentageLabel);
+ } else if (!newValue.equals("")) {
+ if (model.isSellOffer()) {
+ tooltip = Res.get("createOffer.info.sellAboveMarketPrice", newValue);
+ } else {
+ tooltip = Res.get("createOffer.info.buyBelowMarketPrice", newValue);
+ }
+ final Label positivePercentageLabel = createPopoverLabel(tooltip);
+ marketBasedPriceInfoInputTextField.setContentForInfoPopOver(positivePercentageLabel);
+ }
+ }
+ };
+ }
+
+ private Label createPopoverLabel(String text) {
+ final Label label = new Label(text);
+ label.setPrefWidth(300);
+ label.setWrapText(true);
+ label.setPadding(new Insets(10));
+ return label;
+ }
+
+ protected void updateMarketPriceAvailable() {
+ int marketPriceAvailableValue = model.marketPriceAvailableProperty.get();
+ if (marketPriceAvailableValue > -1) {
+ boolean isMarketPriceAvailable = marketPriceAvailableValue == 1;
+ percentagePriceBox.setVisible(isMarketPriceAvailable);
+ percentagePriceBox.setManaged(isMarketPriceAvailable);
+ priceTypeToggleButton.setVisible(isMarketPriceAvailable);
+ priceTypeToggleButton.setManaged(isMarketPriceAvailable);
+ boolean fixedPriceSelected = !model.getDataModel().getUseMarketBasedPrice().get() || !isMarketPriceAvailable;
+ updatePriceToggleButtons(fixedPriceSelected);
+ }
+ }
+
+ private void addListeners() {
+ model.tradeCurrencyCode.addListener(tradeCurrencyCodeListener);
+ model.marketPriceAvailableProperty.addListener(marketPriceAvailableListener);
+ model.marketPriceMargin.addListener(marketPriceMarginListener);
+
+ // focus out
+ amountTextField.focusedProperty().addListener(amountFocusedListener);
+ minAmountTextField.focusedProperty().addListener(minAmountFocusedListener);
+ fixedPriceTextField.focusedProperty().addListener(priceFocusedListener);
+ marketBasedPriceTextField.focusedProperty().addListener(priceAsPercentageFocusedListener);
+ volumeTextField.focusedProperty().addListener(volumeFocusedListener);
+ buyerSecurityDepositInputTextField.focusedProperty().addListener(buyerSecurityDepositFocusedListener);
+
+ // notifications
+ model.getDataModel().getShowWalletFundedNotification().addListener(getShowWalletFundedNotificationListener);
+
+ // warnings
+ model.errorMessage.addListener(errorMessageListener);
+ // model.getDataModel().feeFromFundingTxProperty.addListener(feeFromFundingTxListener);
+
+ model.placeOfferCompleted.addListener(placeOfferCompletedListener);
+
+ // UI actions
+ paymentAccountsComboBox.setOnAction(paymentAccountsComboBoxSelectionHandler);
+ currencyComboBox.setOnAction(currencyComboBoxSelectionHandler);
+ }
+
+ private void removeListeners() {
+ model.tradeCurrencyCode.removeListener(tradeCurrencyCodeListener);
+ model.marketPriceAvailableProperty.removeListener(marketPriceAvailableListener);
+ model.marketPriceMargin.removeListener(marketPriceMarginListener);
+
+ // focus out
+ amountTextField.focusedProperty().removeListener(amountFocusedListener);
+ minAmountTextField.focusedProperty().removeListener(minAmountFocusedListener);
+ fixedPriceTextField.focusedProperty().removeListener(priceFocusedListener);
+ marketBasedPriceTextField.focusedProperty().removeListener(priceAsPercentageFocusedListener);
+ volumeTextField.focusedProperty().removeListener(volumeFocusedListener);
+ buyerSecurityDepositInputTextField.focusedProperty().removeListener(buyerSecurityDepositFocusedListener);
+
+ // notifications
+ model.getDataModel().getShowWalletFundedNotification().removeListener(getShowWalletFundedNotificationListener);
+
+ // warnings
+ model.errorMessage.removeListener(errorMessageListener);
+ // model.getDataModel().feeFromFundingTxProperty.removeListener(feeFromFundingTxListener);
+
+ model.placeOfferCompleted.removeListener(placeOfferCompletedListener);
+
+ // UI actions
+ paymentAccountsComboBox.setOnAction(null);
+ currencyComboBox.setOnAction(null);
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Build UI elements
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ private void addScrollPane() {
+ scrollPane = new ScrollPane();
+ scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
+ scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
+ scrollPane.setFitToWidth(true);
+ scrollPane.setFitToHeight(true);
+ scrollPane.setOnScroll(e -> InputTextField.hideErrorMessageDisplay());
+ AnchorPane.setLeftAnchor(scrollPane, 0d);
+ AnchorPane.setTopAnchor(scrollPane, 0d);
+ AnchorPane.setRightAnchor(scrollPane, 0d);
+ AnchorPane.setBottomAnchor(scrollPane, 0d);
+ root.getChildren().add(scrollPane);
+ }
+
+ private void addGridPane() {
+ gridPane = new GridPane();
+ gridPane.setPadding(new Insets(30, 25, -1, 25));
+ gridPane.setHgap(5);
+ gridPane.setVgap(5);
+ ColumnConstraints columnConstraints1 = new ColumnConstraints();
+ columnConstraints1.setHalignment(HPos.RIGHT);
+ columnConstraints1.setHgrow(Priority.NEVER);
+ columnConstraints1.setMinWidth(200);
+ ColumnConstraints columnConstraints2 = new ColumnConstraints();
+ columnConstraints2.setHgrow(Priority.ALWAYS);
+ ColumnConstraints columnConstraints3 = new ColumnConstraints();
+ columnConstraints3.setHgrow(Priority.NEVER);
+ gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2, columnConstraints3);
+ scrollPane.setContent(gridPane);
+ }
+
+ private void addPaymentGroup() {
+ paymentTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 2, Res.get("shared.selectTradingAccount"));
+ GridPane.setColumnSpan(paymentTitledGroupBg, 3);
+
+ //noinspection unchecked
+ final Tuple2