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 paymentAccountLabelComboBoxTuple = addLabelComboBox(gridPane, gridRow, Res.getWithCol("shared.tradingAccount"), Layout.FIRST_ROW_DISTANCE); + paymentAccountsLabel = paymentAccountLabelComboBoxTuple.first; + paymentAccountsComboBox = paymentAccountLabelComboBoxTuple.second; + paymentAccountsComboBox.setPromptText(Res.get("shared.selectTradingAccount")); + paymentAccountsComboBox.setMinWidth(300); + editOfferElements.add(paymentAccountsComboBox); + + // we display either currencyComboBox (multi currency account) or currencyTextField (single) + Tuple2 currencyComboBoxTuple = addLabelComboBox(gridPane, ++gridRow, Res.getWithCol("shared.currency")); + currencyComboBoxLabel = currencyComboBoxTuple.first; + editOfferElements.add(currencyComboBoxLabel); + //noinspection unchecked + currencyComboBox = currencyComboBoxTuple.second; + editOfferElements.add(currencyComboBox); + currencyComboBox.setPromptText(Res.get("list.currency.select")); + currencyComboBox.setConverter(new StringConverter() { + @Override + public String toString(TradeCurrency tradeCurrency) { + return tradeCurrency.getNameAndCode(); + } + + @Override + public TradeCurrency fromString(String s) { + return null; + } + }); + + Tuple2 currencyTextFieldTuple = addLabelTextField(gridPane, gridRow, Res.getWithCol("shared.currency"), "", 5); + currencyTextFieldLabel = currencyTextFieldTuple.first; + currencyTextFieldLabel.setVisible(false); + editOfferElements.add(currencyTextFieldLabel); + currencyTextField = currencyTextFieldTuple.second; + currencyTextField.setVisible(false); + editOfferElements.add(currencyTextField); + } + + protected void hidePaymentGroup() { + paymentTitledGroupBg.setVisible(false); + paymentAccountsLabel.setVisible(false); + paymentAccountsComboBox.setVisible(false); + currencyComboBox.setVisible(false); + currencyTextFieldLabel.setVisible(false); + currencyTextField.setVisible(false); + } + + private void addAmountPriceGroup() { + TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, Res.get("createOffer.setAmountPrice"), Layout.GROUP_DISTANCE); + GridPane.setColumnSpan(titledGroupBg, 3); + + imageView = new ImageView(); + imageView.setPickOnBounds(true); + directionLabel = new AutoTooltipLabel(); + directionLabel.setAlignment(Pos.CENTER); + directionLabel.setPadding(new Insets(-5, 0, 0, 0)); + directionLabel.setId("direction-icon-label"); + VBox imageVBox = new VBox(); + imageVBox.setAlignment(Pos.CENTER); + imageVBox.setSpacing(12); + imageVBox.getChildren().addAll(imageView, directionLabel); + GridPane.setRowIndex(imageVBox, gridRow); + GridPane.setRowSpan(imageVBox, 2); + GridPane.setMargin(imageVBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 10, 10, 10)); + gridPane.getChildren().add(imageVBox); + + addAmountPriceFields(); + addSecondRow(); + } + + private void addOptionsGroup() { + setDepositTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, Res.get("createOffer.setDeposit"), Layout.GROUP_DISTANCE); + + addBuyerSecurityDepositRow(); + + Tuple2 tuple = add2ButtonsAfterGroup(gridPane, ++gridRow, + Res.get("shared.nextStep"), Res.get("shared.cancel")); + nextButton = tuple.first; + editOfferElements.add(nextButton); + nextButton.disableProperty().bind(model.isNextButtonDisabled); + cancelButton1 = tuple.second; + editOfferElements.add(cancelButton1); + cancelButton1.setDefaultButton(false); + cancelButton1.setOnAction(e -> { + close(); + model.getDataModel().swapTradeToSavings(); + }); + cancelButton1.setId("cancel-button"); + + GridPane.setMargin(nextButton, new Insets(-35, 0, 0, 0)); + nextButton.setOnAction(e -> { + if (model.isPriceInRange()) { + if (DevEnv.DAO_TRADING_ACTIVATED) + showFeeOption(); + else + onShowPayFundsScreen(); + } + }); + } + + protected void hideOptionsGroup() { + setDepositTitledGroupBg.setVisible(false); + setDepositTitledGroupBg.setManaged(false); + nextButton.setVisible(false); + nextButton.setManaged(false); + cancelButton1.setVisible(false); + cancelButton1.setManaged(false); + buyerSecurityDepositLabel.setVisible(false); + buyerSecurityDepositLabel.setManaged(false); + buyerSecurityDepositInputTextField.setVisible(false); + buyerSecurityDepositInputTextField.setManaged(false); + buyerSecurityDepositBtcLabel.setVisible(false); + buyerSecurityDepositBtcLabel.setManaged(false); + } + + private void showFeeOption() { + Coin makerFee = model.getDataModel().getMakerFee(false); + String missingBsq = null; + if (makerFee != null) { + missingBsq = Res.get("popup.warning.insufficientBsqFundsForBtcFeePayment", + bsqFormatter.formatCoinWithCode(makerFee.subtract(model.getDataModel().getBsqBalance()))); + + } else if (model.getDataModel().getBsqBalance().isZero()) + missingBsq = Res.get("popup.warning.noBsqFundsForBtcFeePayment"); + + new FeeOptionWindow(model.makerFeeWithCode, + model.getDataModel().isCurrencyForMakerFeeBtc(), + model.getDataModel().isBsqForFeeAvailable(), + missingBsq, + navigation, + this::onShowPayFundsScreen) + .onSelectionChangedHandler(model::setIsCurrencyForMakerFeeBtc) + .onAction(this::onShowPayFundsScreen) + .hideCloseButton() + .show(); + } + + private void addBuyerSecurityDepositRow() { + final double top = model.getDataModel().isBsqForFeeAvailable() ? 0 : Layout.FIRST_ROW_AND_GROUP_DISTANCE; + buyerSecurityDepositLabel = addLabel(gridPane, ++gridRow, + Res.getWithCol("shared.securityDepositBox.description", Res.get("shared.buyer")), + top); + + Tuple3 tuple = getEditableValueCurrencyBox( + Res.get("createOffer.securityDeposit.prompt")); + buyerSecurityDepositValueCurrencyBox = tuple.first; + buyerSecurityDepositInputTextField = tuple.second; + buyerSecurityDepositBtcLabel = tuple.third; + + editOfferElements.add(buyerSecurityDepositInputTextField); + editOfferElements.add(buyerSecurityDepositBtcLabel); + + GridPane.setRowIndex(buyerSecurityDepositValueCurrencyBox, gridRow); + GridPane.setColumnIndex(buyerSecurityDepositValueCurrencyBox, 1); + GridPane.setColumnSpan(buyerSecurityDepositValueCurrencyBox, 2); + GridPane.setMargin(buyerSecurityDepositValueCurrencyBox, new Insets(top, 0, 0, 0)); + gridPane.getChildren().add(buyerSecurityDepositValueCurrencyBox); + } + + private void addFundingGroup() { + // don't increase gridRow as we removed button when this gets visible + payFundsTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 3, + Res.get("createOffer.fundsBox.title"), Layout.GROUP_DISTANCE); + GridPane.setColumnSpan(payFundsTitledGroupBg, 3); + payFundsTitledGroupBg.setVisible(false); + + Tuple2 fundsTuple = addLabelFundsTextfield(gridPane, gridRow, + Res.get("shared.totalsNeeded"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); + totalToPayLabel = fundsTuple.first; + totalToPayLabel.setVisible(false); + totalToPayTextField = fundsTuple.second; + totalToPayTextField.setVisible(false); + + qrCodeImageView = new ImageView(); + qrCodeImageView.setVisible(false); + qrCodeImageView.getStyleClass().add("qr-code"); + Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow"))); + qrCodeImageView.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute( + () -> UserThread.runAfter( + () -> new QRCodeWindow(getBitcoinURI()).show(), + 200, TimeUnit.MILLISECONDS))); + GridPane.setRowIndex(qrCodeImageView, gridRow); + GridPane.setColumnIndex(qrCodeImageView, 2); + GridPane.setRowSpan(qrCodeImageView, 3); + GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE - 9, 0, 0, 5)); + gridPane.getChildren().add(qrCodeImageView); + + Tuple2 addressTuple = addLabelAddressTextField(gridPane, ++gridRow, + Res.get("shared.tradeWalletAddress")); + addressLabel = addressTuple.first; + addressLabel.setVisible(false); + addressTextField = addressTuple.second; + addressTextField.setVisible(false); + + Tuple2 balanceTuple = addLabelBalanceTextField(gridPane, ++gridRow, + Res.get("shared.tradeWalletBalance")); + balanceLabel = balanceTuple.first; + balanceLabel.setVisible(false); + balanceTextField = balanceTuple.second; + balanceTextField.setVisible(false); + + fundingHBox = new HBox(); + fundingHBox.setVisible(false); + fundingHBox.setManaged(false); + fundingHBox.setSpacing(10); + Button fundFromSavingsWalletButton = new AutoTooltipButton(Res.get("shared.fundFromSavingsWalletButton")); + fundFromSavingsWalletButton.setDefaultButton(false); + fundFromSavingsWalletButton.setOnAction(e -> model.fundFromSavingsWallet()); + Label label = new AutoTooltipLabel(Res.get("shared.OR")); + label.setPadding(new Insets(5, 0, 0, 0)); + Button fundFromExternalWalletButton = new AutoTooltipButton(Res.get("shared.fundFromExternalWalletButton")); + fundFromExternalWalletButton.setDefaultButton(false); + fundFromExternalWalletButton.setOnAction(e -> GUIUtil.showFeeInfoBeforeExecute(this::openWallet)); + waitingForFundsBusyAnimation = new BusyAnimation(); + waitingForFundsLabel = new AutoTooltipLabel(); + waitingForFundsLabel.setPadding(new Insets(5, 0, 0, 0)); + + fundingHBox.getChildren().addAll(fundFromSavingsWalletButton, label, fundFromExternalWalletButton, waitingForFundsBusyAnimation, waitingForFundsLabel); + GridPane.setRowIndex(fundingHBox, ++gridRow); + GridPane.setColumnIndex(fundingHBox, 1); + GridPane.setMargin(fundingHBox, new Insets(15, 10, 0, 0)); + gridPane.getChildren().add(fundingHBox); + + + placeOfferButton = addButtonAfterGroup(gridPane, gridRow, ""); + placeOfferButton.setOnAction(e -> onPlaceOffer()); + placeOfferButton.setMinHeight(40); + placeOfferButton.setPadding(new Insets(0, 20, 0, 20)); + + cancelButton2 = addButton(gridPane, ++gridRow, Res.get("shared.cancel")); + cancelButton2.setOnAction(e -> { + if (model.getDataModel().getIsBtcWalletFunded().get()) { + new Popup<>().warning(Res.get("createOffer.warnCancelOffer")) + .closeButtonText(Res.get("shared.no")) + .actionButtonText(Res.get("shared.yesCancel")) + .onAction(() -> { + close(); + model.getDataModel().swapTradeToSavings(); + }) + .show(); + } else { + close(); + model.getDataModel().swapTradeToSavings(); + } + }); + cancelButton2.setDefaultButton(false); + cancelButton2.setVisible(false); + } + + private void openWallet() { + try { + Utilities.openURI(URI.create(getBitcoinURI())); + } catch (Exception ex) { + log.warn(ex.getMessage()); + new Popup<>().warning(Res.get("shared.openDefaultWalletFailed")).show(); + } + } + + @NotNull + private String getBitcoinURI() { + return GUIUtil.getBitcoinURI(addressTextField.getAddress(), model.getDataModel().getMissingCoin().get(), + model.getPaymentLabel()); + } + + private void addAmountPriceFields() { + // amountBox + Tuple3 amountValueCurrencyBoxTuple = getEditableValueCurrencyBox(Res.get("createOffer.amount.prompt")); + HBox amountValueCurrencyBox = amountValueCurrencyBoxTuple.first; + amountTextField = amountValueCurrencyBoxTuple.second; + editOfferElements.add(amountTextField); + Label amountBtcLabel = amountValueCurrencyBoxTuple.third; + editOfferElements.add(amountBtcLabel); + Tuple2 amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, model.getAmountDescription()); + amountDescriptionLabel = amountInputBoxTuple.first; + editOfferElements.add(amountDescriptionLabel); + VBox amountBox = amountInputBoxTuple.second; + + // x + xLabel = new AutoTooltipLabel(); + xLabel.setFont(Font.font("Helvetica-Bold", 20)); + xLabel.setPadding(new Insets(14, 3, 0, 3)); + xLabel.setMinWidth(14); + xLabel.setMaxWidth(14); + + // price as percent + Tuple3 priceAsPercentageTuple = getEditableValueCurrencyBoxWithInfo(Res.get("createOffer.price.prompt")); + + HBox priceAsPercentageValueCurrencyBox = priceAsPercentageTuple.first; + marketBasedPriceInfoInputTextField = priceAsPercentageTuple.second; + marketBasedPriceTextField = marketBasedPriceInfoInputTextField.getTextField(); + marketBasedPriceTextField.setPrefWidth(200); + editOfferElements.add(marketBasedPriceTextField); + marketBasedPriceLabel = priceAsPercentageTuple.third; + editOfferElements.add(marketBasedPriceLabel); + Tuple2 priceAsPercentageInputBoxTuple = getTradeInputBox(priceAsPercentageValueCurrencyBox, + Res.get("shared.distanceInPercent")); + percentagePriceDescription = priceAsPercentageInputBoxTuple.first; + percentagePriceDescription.setPrefWidth(200); + + getSmallIconForLabel(MaterialDesignIcon.CHART_LINE, percentagePriceDescription); + + percentagePriceBox = priceAsPercentageInputBoxTuple.second; + + // Fixed/Percentage toggle + priceTypeToggleButton = getIconButton(MaterialDesignIcon.SWAP_VERTICAL); + editOfferElements.add(priceTypeToggleButton); + + priceTypeToggleButton.setOnAction((actionEvent) -> { + updatePriceToggleButtons(model.getDataModel().getUseMarketBasedPrice().getValue()); + }); + + // = + resultLabel = new AutoTooltipLabel("="); + resultLabel.setFont(Font.font("Helvetica-Bold", 20)); + resultLabel.setPadding(new Insets(14, 2, 0, 2)); + + // volume + Tuple3 volumeValueCurrencyBoxTuple = getEditableValueCurrencyBox(Res.get("createOffer.volume.prompt")); + HBox volumeValueCurrencyBox = volumeValueCurrencyBoxTuple.first; + volumeTextField = volumeValueCurrencyBoxTuple.second; + editOfferElements.add(volumeTextField); + volumeCurrencyLabel = volumeValueCurrencyBoxTuple.third; + editOfferElements.add(volumeCurrencyLabel); + Tuple2 volumeInputBoxTuple = getTradeInputBox(volumeValueCurrencyBox, model.volumeDescriptionLabel.get()); + volumeDescriptionLabel = volumeInputBoxTuple.first; + editOfferElements.add(volumeDescriptionLabel); + VBox volumeBox = volumeInputBoxTuple.second; + + firstRowHBox = new HBox(); + firstRowHBox.setSpacing(5); + firstRowHBox.setAlignment(Pos.CENTER_LEFT); + firstRowHBox.getChildren().addAll(amountBox, xLabel, percentagePriceBox, priceTypeToggleButton, resultLabel, volumeBox); + GridPane.setRowIndex(firstRowHBox, gridRow); + GridPane.setColumnIndex(firstRowHBox, 1); + GridPane.setMargin(firstRowHBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 10, 0, 0)); + GridPane.setColumnSpan(firstRowHBox, 2); + gridPane.getChildren().add(firstRowHBox); + } + + private void updatePriceToggleButtons(boolean fixedPriceSelected) { + int marketPriceAvailable = model.marketPriceAvailableProperty.get(); + fixedPriceSelected = fixedPriceSelected || (marketPriceAvailable == 0); + + if (marketPriceAvailable == 1) { + model.getDataModel().setUseMarketBasedPrice(!fixedPriceSelected); + } + + percentagePriceBox.setDisable(fixedPriceSelected); + fixedPriceBox.setDisable(!fixedPriceSelected); + + if (fixedPriceSelected) { + if (firstRowHBox.getChildren().contains(percentagePriceBox)) + firstRowHBox.getChildren().remove(percentagePriceBox); + if (secondRowHBox.getChildren().contains(fixedPriceBox)) + secondRowHBox.getChildren().remove(fixedPriceBox); + if (!firstRowHBox.getChildren().contains(fixedPriceBox)) + firstRowHBox.getChildren().add(2, fixedPriceBox); + if (!secondRowHBox.getChildren().contains(percentagePriceBox)) + secondRowHBox.getChildren().add(2, percentagePriceBox); + } else { + if (firstRowHBox.getChildren().contains(fixedPriceBox)) + firstRowHBox.getChildren().remove(fixedPriceBox); + if (secondRowHBox.getChildren().contains(percentagePriceBox)) + secondRowHBox.getChildren().remove(percentagePriceBox); + if (!firstRowHBox.getChildren().contains(percentagePriceBox)) + firstRowHBox.getChildren().add(2, percentagePriceBox); + if (!secondRowHBox.getChildren().contains(fixedPriceBox)) + secondRowHBox.getChildren().add(2, fixedPriceBox); + } + } + + private void addSecondRow() { + // price as fiat + Tuple3 priceValueCurrencyBoxTuple = getEditableValueCurrencyBox( + Res.get("createOffer.price.prompt")); + HBox priceValueCurrencyBox = priceValueCurrencyBoxTuple.first; + fixedPriceTextField = priceValueCurrencyBoxTuple.second; + fixedPriceTextField.setPrefWidth(200); + editOfferElements.add(fixedPriceTextField); + priceCurrencyLabel = priceValueCurrencyBoxTuple.third; + editOfferElements.add(priceCurrencyLabel); + Tuple2 priceInputBoxTuple = getTradeInputBox(priceValueCurrencyBox, ""); + priceDescriptionLabel = priceInputBoxTuple.first; + + priceDescriptionLabel.setPrefWidth(200); + + getSmallIconForLabel(MaterialDesignIcon.LOCK, priceDescriptionLabel); + + editOfferElements.add(priceDescriptionLabel); + fixedPriceBox = priceInputBoxTuple.second; + + marketBasedPriceTextField.setPromptText(Res.get("shared.enterPercentageValue")); + marketBasedPriceLabel.setText("%"); + marketBasedPriceLabel.getStyleClass().add("percentage-label"); + + Tuple3 amountValueCurrencyBoxTuple = getEditableValueCurrencyBox( + Res.get("createOffer.amount.prompt")); + HBox amountValueCurrencyBox = amountValueCurrencyBoxTuple.first; + minAmountTextField = amountValueCurrencyBoxTuple.second; + editOfferElements.add(minAmountTextField); + Label minAmountBtcLabel = amountValueCurrencyBoxTuple.third; + editOfferElements.add(minAmountBtcLabel); + + Tuple2 amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, + Res.get("createOffer.amountPriceBox.minAmountDescription")); + + Label xLabel = new AutoTooltipLabel("x"); + xLabel.setFont(Font.font("Helvetica-Bold", 20)); + xLabel.setPadding(new Insets(14, 3, 0, 3)); + xLabel.setVisible(false); // we just use it to get the same layout as the upper row + + secondRowHBox = new HBox(); + secondRowHBox.setSpacing(5); + secondRowHBox.setAlignment(Pos.CENTER_LEFT); + secondRowHBox.getChildren().addAll(amountInputBoxTuple.second, xLabel, fixedPriceBox, priceTypeToggleButton); + GridPane.setRowIndex(secondRowHBox, ++gridRow); + GridPane.setColumnIndex(secondRowHBox, 1); + GridPane.setMargin(secondRowHBox, new Insets(0, 10, 0, 0)); + GridPane.setColumnSpan(secondRowHBox, 2); + gridPane.getChildren().add(secondRowHBox); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PayInfo + /////////////////////////////////////////////////////////////////////////////////////////// + + private GridPane createInfoPopover() { + GridPane infoGridPane = new GridPane(); + infoGridPane.setHgap(5); + infoGridPane.setVgap(5); + infoGridPane.setPadding(new Insets(10, 10, 10, 10)); + + int i = 0; + if (model.isSellOffer()) + addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.tradeAmount"), model.tradeAmount.get()); + + addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.yourSecurityDeposit"), model.getSecurityDepositInfo()); + addPayInfoEntry(infoGridPane, i++, Res.get("createOffer.fundsBox.offerFee"), model.getMakerFee()); + addPayInfoEntry(infoGridPane, i++, Res.get("createOffer.fundsBox.networkFee"), model.getTxFee()); + Separator separator = new Separator(); + separator.setOrientation(Orientation.HORIZONTAL); + separator.getStyleClass().add("offer-separator"); + GridPane.setConstraints(separator, 1, i++); + infoGridPane.getChildren().add(separator); + addPayInfoEntry(infoGridPane, i, Res.getWithCol("shared.total"), model.getTotalToPayInfo()); + return infoGridPane; + } + + private void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) { + Label label = new AutoTooltipLabel(labelText); + TextField textField = new TextField(value); + textField.setMinWidth(500); + textField.setEditable(false); + textField.setFocusTraversable(false); + textField.setId("payment-info"); + GridPane.setConstraints(label, 0, row, 1, 1, HPos.RIGHT, VPos.CENTER); + GridPane.setConstraints(textField, 1, row); + infoGridPane.getChildren().addAll(label, textField); + } +} diff --git a/src/main/java/bisq/desktop/main/offer/EditableOfferViewModel.java b/src/main/java/bisq/desktop/main/offer/EditableOfferViewModel.java new file mode 100644 index 00000000000..c7f58eac344 --- /dev/null +++ b/src/main/java/bisq/desktop/main/offer/EditableOfferViewModel.java @@ -0,0 +1,1073 @@ +/* + * 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.model.ActivatableWithDataModel; +import bisq.desktop.main.MainView; +import bisq.desktop.main.funds.FundsView; +import bisq.desktop.main.funds.deposit.DepositView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.settings.SettingsView; +import bisq.desktop.main.settings.preferences.PreferencesView; +import bisq.desktop.util.BSFormatter; +import bisq.desktop.util.BsqFormatter; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.validation.AltcoinValidator; +import bisq.desktop.util.validation.BsqValidator; +import bisq.desktop.util.validation.BtcValidator; +import bisq.desktop.util.validation.FiatPriceValidator; +import bisq.desktop.util.validation.FiatVolumeValidator; +import bisq.desktop.util.validation.MonetaryValidator; +import bisq.desktop.util.validation.SecurityDepositValidator; + +import bisq.core.app.BisqEnvironment; +import bisq.core.btc.Restrictions; +import bisq.core.btc.wallet.WalletsSetup; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.locale.TradeCurrency; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.payment.PaymentAccount; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.user.Preferences; +import bisq.core.util.validation.InputValidator; + +import bisq.network.p2p.P2PService; + +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.util.MathUtils; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.Fiat; + +import javax.inject.Inject; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.beans.value.ChangeListener; + +import java.util.concurrent.TimeUnit; + +import static javafx.beans.binding.Bindings.createStringBinding; + +public abstract class EditableOfferViewModel extends ActivatableWithDataModel { + private final BtcValidator btcValidator; + private final BsqValidator bsqValidator; + private final SecurityDepositValidator securityDepositValidator; + private final P2PService p2PService; + private final WalletsSetup walletsSetup; + private final PriceFeedService priceFeedService; + private final Navigation navigation; + private final Preferences preferences; + protected final BSFormatter btcFormatter; + private final BsqFormatter bsqFormatter; + private final FiatVolumeValidator fiatVolumeValidator; + private final FiatPriceValidator fiatPriceValidator; + private final AltcoinValidator altcoinValidator; + + private String amountDescription; + private String directionLabel; + private String addressAsString; + private final String paymentLabel; + private boolean createOfferRequested; + + public final StringProperty amount = new SimpleStringProperty(); + public final StringProperty minAmount = new SimpleStringProperty(); + final StringProperty buyerSecurityDeposit = new SimpleStringProperty(); + final String sellerSecurityDeposit; + + // Price in the viewModel is always dependent on fiat/altcoin: Fiat Fiat/BTC, for altcoins we use inverted price. + // The domain (dataModel) uses always the same price model (otherCurrencyBTC) + // If we would change the price representation in the domain we would not be backward compatible + public final StringProperty price = new SimpleStringProperty(); + final StringProperty makerFee = new SimpleStringProperty(); + final StringProperty makerFeeWithCode = new SimpleStringProperty(); + final StringProperty makerFeeCurrencyCode = new SimpleStringProperty(); + + // Positive % value means always a better price form the maker's perspective: + // Buyer (with fiat): lower price as market + // Buyer (with altcoin): higher (display) price as market (display price is inverted) + public final StringProperty marketPriceMargin = new SimpleStringProperty(); + public final StringProperty volume = new SimpleStringProperty(); + final StringProperty volumeDescriptionLabel = new SimpleStringProperty(); + final StringProperty volumePromptLabel = new SimpleStringProperty(); + final StringProperty tradeAmount = new SimpleStringProperty(); + final StringProperty totalToPay = new SimpleStringProperty(); + final StringProperty errorMessage = new SimpleStringProperty(); + final StringProperty tradeCurrencyCode = new SimpleStringProperty(); + final StringProperty waitingForFundsText = new SimpleStringProperty(""); + + final BooleanProperty isPlaceOfferButtonDisabled = new SimpleBooleanProperty(true); + final BooleanProperty cancelButtonDisabled = new SimpleBooleanProperty(); + public final BooleanProperty isNextButtonDisabled = new SimpleBooleanProperty(true); + final BooleanProperty placeOfferCompleted = new SimpleBooleanProperty(); + final BooleanProperty showPayFundsScreenDisplayed = new SimpleBooleanProperty(); + private final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); + final BooleanProperty isWaitingForFunds = new SimpleBooleanProperty(); + + final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty minAmountValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty priceValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty buyerSecurityDepositValidationResult = new SimpleObjectProperty<>(); + + // Those are needed for the addressTextField + private final ObjectProperty
address = new SimpleObjectProperty<>(); + + private ChangeListener amountStringListener; + private ChangeListener minAmountStringListener; + private ChangeListener priceStringListener, marketPriceMarginStringListener; + private ChangeListener volumeStringListener; + private ChangeListener securityDepositStringListener; + + private ChangeListener amountAsCoinListener; + private ChangeListener minAmountAsCoinListener; + private ChangeListener priceListener; + private ChangeListener volumeListener; + private ChangeListener securityDepositAsCoinListener; + + private ChangeListener isWalletFundedListener; + //private ChangeListener feeFromFundingTxListener; + private ChangeListener errorMessageListener; + private Offer offer; + private Timer timeoutTimer; + private boolean inputIsMarketBasedPrice; + private ChangeListener useMarketBasedPriceListener; + private boolean ignorePriceStringListener, ignoreVolumeStringListener, ignoreAmountStringListener, ignoreSecurityDepositStringListener; + private MarketPrice marketPrice; + final IntegerProperty marketPriceAvailableProperty = new SimpleIntegerProperty(-1); + private ChangeListener currenciesUpdateListener; + private boolean syncMinAmountWithAmount = true; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public EditableOfferViewModel(M dataModel, + FiatVolumeValidator fiatVolumeValidator, + FiatPriceValidator fiatPriceValidator, + AltcoinValidator altcoinValidator, + BtcValidator btcValidator, + BsqValidator bsqValidator, + SecurityDepositValidator securityDepositValidator, + P2PService p2PService, + WalletsSetup walletsSetup, + PriceFeedService priceFeedService, + Navigation navigation, + Preferences preferences, + BSFormatter btcFormatter, + BsqFormatter bsqFormatter) { + super(dataModel); + + this.fiatVolumeValidator = fiatVolumeValidator; + this.fiatPriceValidator = fiatPriceValidator; + this.altcoinValidator = altcoinValidator; + this.btcValidator = btcValidator; + this.bsqValidator = bsqValidator; + this.securityDepositValidator = securityDepositValidator; + this.p2PService = p2PService; + this.walletsSetup = walletsSetup; + this.priceFeedService = priceFeedService; + this.navigation = navigation; + this.preferences = preferences; + this.btcFormatter = btcFormatter; + this.bsqFormatter = bsqFormatter; + + paymentLabel = Res.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId); + sellerSecurityDeposit = btcFormatter.formatCoin(dataModel.getSellerSecurityDeposit()); + + if (dataModel.getAddressEntry() != null) { + addressAsString = dataModel.getAddressEntry().getAddressString(); + address.set(dataModel.getAddressEntry().getAddress()); + } + createListeners(); + } + + @Override + public void activate() { + if (DevEnv.isDevMode()) { + UserThread.runAfter(() -> { + switch (BisqEnvironment.getBaseCurrencyNetwork().getCurrencyCode()) { + case "BTC": + amount.set("0.0001"); + price.set("11029"); + break; + case "LTC": + amount.set("50"); + price.set("40"); + break; + case "DASH": + amount.set("0.1"); + price.set("40"); + break; + } + + minAmount.set(amount.get()); + onFocusOutPriceAsPercentageTextField(true, false); + applyMakerFee(); + updateButtonDisableState(); + setAmountToModel(); + setMinAmountToModel(); + setPriceToModel(); + dataModel.calculateVolume(); + dataModel.calculateTotalToPay(); + updateButtonDisableState(); + updateSpinnerInfo(); + }, 100, TimeUnit.MILLISECONDS); + } + + addBindings(); + addListeners(); + + updateButtonDisableState(); + + updateMarketPriceAvailable(); + } + + @Override + protected void deactivate() { + removeBindings(); + removeListeners(); + stopTimeoutTimer(); + } + + private void addBindings() { + if (dataModel.getDirection() == OfferPayload.Direction.BUY) { + volumeDescriptionLabel.bind(createStringBinding( + () -> Res.get("createOffer.amountPriceBox.buy.volumeDescription", dataModel.getTradeCurrencyCode().get()), + dataModel.getTradeCurrencyCode())); + } else { + volumeDescriptionLabel.bind(createStringBinding( + () -> Res.get("createOffer.amountPriceBox.sell.volumeDescription", dataModel.getTradeCurrencyCode().get()), + dataModel.getTradeCurrencyCode())); + } + volumePromptLabel.bind(createStringBinding( + () -> Res.get("createOffer.volume.prompt", dataModel.getTradeCurrencyCode().get()), + dataModel.getTradeCurrencyCode())); + + totalToPay.bind(createStringBinding(() -> btcFormatter.formatCoinWithCode(dataModel.totalToPayAsCoinProperty().get()), + dataModel.totalToPayAsCoinProperty())); + + + tradeAmount.bind(createStringBinding(() -> btcFormatter.formatCoinWithCode(dataModel.getAmount().get()), + dataModel.getAmount())); + + + tradeCurrencyCode.bind(dataModel.getTradeCurrencyCode()); + } + + private void removeBindings() { + totalToPay.unbind(); + tradeAmount.unbind(); + tradeCurrencyCode.unbind(); + volumeDescriptionLabel.unbind(); + volumePromptLabel.unbind(); + } + + private void createListeners() { + amountStringListener = (ov, oldValue, newValue) -> { + if (!ignoreAmountStringListener) { + if (isBtcInputValid(newValue).isValid) { + setAmountToModel(); + dataModel.calculateVolume(); + dataModel.calculateTotalToPay(); + } + updateButtonDisableState(); + } + }; + minAmountStringListener = (ov, oldValue, newValue) -> { + if (isBtcInputValid(newValue).isValid) + setMinAmountToModel(); + updateButtonDisableState(); + }; + priceStringListener = (ov, oldValue, newValue) -> { + updateMarketPriceAvailable(); + final String currencyCode = dataModel.getTradeCurrencyCode().get(); + if (!ignorePriceStringListener) { + if (isPriceInputValid(newValue).isValid) { + setPriceToModel(); + dataModel.calculateVolume(); + dataModel.calculateTotalToPay(); + + if (!inputIsMarketBasedPrice) { + if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { + double marketPriceAsDouble = marketPrice.getPrice(); + try { + double priceAsDouble = btcFormatter.parseNumberStringToDouble(price.get()); + double relation = priceAsDouble / marketPriceAsDouble; + final OfferPayload.Direction compareDirection = CurrencyUtil.isCryptoCurrency(currencyCode) ? + OfferPayload.Direction.SELL : + OfferPayload.Direction.BUY; + double percentage = dataModel.getDirection() == compareDirection ? 1 - relation : relation - 1; + percentage = MathUtils.roundDouble(percentage, 4); + dataModel.setMarketPriceMargin(percentage); + marketPriceMargin.set(btcFormatter.formatToPercent(percentage)); + applyMakerFee(); + } catch (NumberFormatException t) { + marketPriceMargin.set(""); + new Popup<>().warning(Res.get("validation.NaN")).show(); + } + } else { + log.debug("We don't have a market price. We use the static price instead."); + } + } + } + } + updateButtonDisableState(); + }; + marketPriceMarginStringListener = (ov, oldValue, newValue) -> { + if (inputIsMarketBasedPrice) { + try { + if (!newValue.isEmpty() && !newValue.equals("-")) { + double percentage = btcFormatter.parsePercentStringToDouble(newValue); + if (percentage >= 1 || percentage <= -1) { + new Popup<>().warning(Res.get("popup.warning.tooLargePercentageValue") + "\n" + + Res.get("popup.warning.examplePercentageValue")) + .show(); + } else { + final String currencyCode = dataModel.getTradeCurrencyCode().get(); + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { + percentage = MathUtils.roundDouble(percentage, 4); + double marketPriceAsDouble = marketPrice.getPrice(); + final boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode); + final OfferPayload.Direction compareDirection = isCryptoCurrency ? + OfferPayload.Direction.SELL : + OfferPayload.Direction.BUY; + double factor = dataModel.getDirection() == compareDirection ? + 1 - percentage : + 1 + percentage; + double targetPrice = marketPriceAsDouble * factor; + int precision = isCryptoCurrency ? + Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; + // protect from triggering unwanted updates + ignorePriceStringListener = true; + price.set(btcFormatter.formatRoundedDoubleWithPrecision(targetPrice, precision)); + ignorePriceStringListener = false; + setPriceToModel(); + dataModel.setMarketPriceMargin(percentage); + dataModel.calculateVolume(); + dataModel.calculateTotalToPay(); + updateButtonDisableState(); + applyMakerFee(); + } else { + new Popup<>().warning(Res.get("popup.warning.noPriceFeedAvailable")).show(); + marketPriceMargin.set(""); + } + } + } + } catch (NumberFormatException t) { + log.error(t.toString()); + t.printStackTrace(); + new Popup<>().warning(Res.get("validation.NaN")).show(); + } catch (Throwable t) { + log.error(t.toString()); + t.printStackTrace(); + new Popup<>().warning(Res.get("validation.inputError", t.toString())).show(); + } + } + }; + useMarketBasedPriceListener = (observable, oldValue, newValue) -> { + if (newValue) + priceValidationResult.set(new InputValidator.ValidationResult(true)); + }; + + volumeStringListener = (ov, oldValue, newValue) -> { + if (!ignoreVolumeStringListener) { + if (isVolumeInputValid(newValue).isValid) { + setVolumeToModel(); + setPriceToModel(); + dataModel.calculateAmount(); + dataModel.calculateTotalToPay(); + } + updateButtonDisableState(); + } + }; + securityDepositStringListener = (ov, oldValue, newValue) -> { + if (!ignoreSecurityDepositStringListener) { + if (securityDepositValidator.validate(newValue).isValid) { + setBuyerSecurityDepositToModel(); + dataModel.calculateTotalToPay(); + } + updateButtonDisableState(); + } + }; + + + amountAsCoinListener = (ov, oldValue, newValue) -> { + if (newValue != null) + amount.set(btcFormatter.formatCoin(newValue)); + else + amount.set(""); + + applyMakerFee(); + }; + minAmountAsCoinListener = (ov, oldValue, newValue) -> { + if (newValue != null) + minAmount.set(btcFormatter.formatCoin(newValue)); + else + minAmount.set(""); + }; + priceListener = (ov, oldValue, newValue) -> { + ignorePriceStringListener = true; + if (newValue != null) + price.set(btcFormatter.formatPrice(newValue)); + else + price.set(""); + + ignorePriceStringListener = false; + applyMakerFee(); + }; + volumeListener = (ov, oldValue, newValue) -> { + ignoreVolumeStringListener = true; + if (newValue != null) + volume.set(btcFormatter.formatVolume(newValue)); + else + volume.set(""); + + ignoreVolumeStringListener = false; + applyMakerFee(); + }; + + securityDepositAsCoinListener = (ov, oldValue, newValue) -> { + if (newValue != null) + buyerSecurityDeposit.set(btcFormatter.formatCoin(newValue)); + else + buyerSecurityDeposit.set(""); + }; + + + isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); + /* feeFromFundingTxListener = (ov, oldValue, newValue) -> { + updateButtonDisableState(); + };*/ + + currenciesUpdateListener = (observable, oldValue, newValue) -> { + updateMarketPriceAvailable(); + updateButtonDisableState(); + }; + } + + private void applyMakerFee() { + makerFee.set(getFormatterForMakerFee().formatCoin(dataModel.getMakerFee())); + makerFeeWithCode.set(getFormatterForMakerFee().formatCoinWithCode(dataModel.getMakerFee())); + makerFeeCurrencyCode.set(dataModel.isCurrencyForMakerFeeBtc() ? Res.getBaseCurrencyCode() : "BSQ"); + } + + private void updateMarketPriceAvailable() { + marketPrice = priceFeedService.getMarketPrice(dataModel.getTradeCurrencyCode().get()); + marketPriceAvailableProperty.set(marketPrice == null || !marketPrice.isExternallyProvidedPrice() ? 0 : 1); + dataModel.setMarketPriceAvailable(marketPrice != null && marketPrice.isExternallyProvidedPrice()); + } + + private void addListeners() { + // Bidirectional bindings are used for all input fields: amount, price, volume and minAmount + // We do volume/amount calculation during input, so user has immediate feedback + amount.addListener(amountStringListener); + minAmount.addListener(minAmountStringListener); + price.addListener(priceStringListener); + marketPriceMargin.addListener(marketPriceMarginStringListener); + dataModel.getUseMarketBasedPrice().addListener(useMarketBasedPriceListener); + volume.addListener(volumeStringListener); + buyerSecurityDeposit.addListener(securityDepositStringListener); + + // Binding with Bindings.createObjectBinding does not work because of bi-directional binding + dataModel.getAmount().addListener(amountAsCoinListener); + dataModel.getMinAmount().addListener(minAmountAsCoinListener); + dataModel.getPrice().addListener(priceListener); + dataModel.getVolume().addListener(volumeListener); + dataModel.getBuyerSecurityDeposit().addListener(securityDepositAsCoinListener); + + // dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener); + dataModel.getIsBtcWalletFunded().addListener(isWalletFundedListener); + + priceFeedService.updateCounterProperty().addListener(currenciesUpdateListener); + } + + private void removeListeners() { + amount.removeListener(amountStringListener); + minAmount.removeListener(minAmountStringListener); + price.removeListener(priceStringListener); + marketPriceMargin.removeListener(marketPriceMarginStringListener); + dataModel.getUseMarketBasedPrice().removeListener(useMarketBasedPriceListener); + volume.removeListener(volumeStringListener); + buyerSecurityDeposit.removeListener(securityDepositStringListener); + + // Binding with Bindings.createObjectBinding does not work because of bi-directional binding + dataModel.getAmount().removeListener(amountAsCoinListener); + dataModel.getMinAmount().removeListener(minAmountAsCoinListener); + dataModel.getPrice().removeListener(priceListener); + dataModel.getVolume().removeListener(volumeListener); + dataModel.getBuyerSecurityDeposit().removeListener(securityDepositAsCoinListener); + + //dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); + dataModel.getIsBtcWalletFunded().removeListener(isWalletFundedListener); + + if (offer != null && errorMessageListener != null) + offer.getErrorMessageProperty().removeListener(errorMessageListener); + + priceFeedService.updateCounterProperty().removeListener(currenciesUpdateListener); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + boolean initWithData(OfferPayload.Direction direction, TradeCurrency tradeCurrency) { + boolean result = dataModel.initWithData(direction, tradeCurrency); + if (dataModel.paymentAccount != null) + btcValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin(dataModel.getTradeCurrencyCode().get())); + btcValidator.setMaxTradeLimit(Coin.valueOf(dataModel.getMaxTradeLimit())); + btcValidator.setMinValue(Restrictions.getMinTradeAmount()); + + final boolean isBuy = dataModel.getDirection() == OfferPayload.Direction.BUY; + directionLabel = isBuy ? Res.get("shared.buyBitcoin") : Res.get("shared.sellBitcoin"); + amountDescription = Res.get("createOffer.amountPriceBox.amountDescription", + isBuy ? Res.get("shared.buy") : Res.get("shared.sell")); + + buyerSecurityDeposit.set(btcFormatter.formatCoin(dataModel.getBuyerSecurityDeposit().get())); + + applyMakerFee(); + return result; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // UI actions + /////////////////////////////////////////////////////////////////////////////////////////// + + void onPlaceOffer(Offer offer, Runnable resultHandler) { + errorMessage.set(null); + createOfferRequested = true; + + if (timeoutTimer == null) { + timeoutTimer = UserThread.runAfter(() -> { + stopTimeoutTimer(); + createOfferRequested = false; + errorMessage.set(Res.get("createOffer.timeoutAtPublishing")); + + updateButtonDisableState(); + updateSpinnerInfo(); + + resultHandler.run(); + }, 60); + } + errorMessageListener = (observable, oldValue, newValue) -> { + if (newValue != null) { + stopTimeoutTimer(); + createOfferRequested = false; + if (offer.getState() == Offer.State.OFFER_FEE_PAID) + errorMessage.set(newValue + Res.get("createOffer.errorInfo")); + else + errorMessage.set(newValue); + + updateButtonDisableState(); + updateSpinnerInfo(); + + resultHandler.run(); + } + }; + + offer.errorMessageProperty().addListener(errorMessageListener); + + dataModel.onPlaceOffer(offer, transaction -> { + stopTimeoutTimer(); + resultHandler.run(); + placeOfferCompleted.set(true); + errorMessage.set(null); + }); + + updateButtonDisableState(); + updateSpinnerInfo(); + } + + public void onPaymentAccountSelected(PaymentAccount paymentAccount) { + dataModel.onPaymentAccountSelected(paymentAccount); + if (amount.get() != null) + amountValidationResult.set(isBtcInputValid(amount.get())); + + btcValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin(dataModel.getTradeCurrencyCode().get())); + btcValidator.setMaxTradeLimit(Coin.valueOf(dataModel.getMaxTradeLimit())); + } + + public void onCurrencySelected(TradeCurrency tradeCurrency) { + dataModel.onCurrencySelected(tradeCurrency); + + marketPrice = priceFeedService.getMarketPrice(dataModel.getTradeCurrencyCode().get()); + marketPriceAvailableProperty.set(marketPrice == null || !marketPrice.isExternallyProvidedPrice() ? 0 : 1); + updateButtonDisableState(); + } + + void onShowPayFundsScreen() { + dataModel.estimateTxSize(); + dataModel.requestTxFee(); + showPayFundsScreenDisplayed.set(true); + updateSpinnerInfo(); + } + + boolean fundFromSavingsWallet() { + dataModel.fundFromSavingsWallet(); + if (dataModel.getIsBtcWalletFunded().get()) { + updateButtonDisableState(); + return true; + } else { + //noinspection unchecked + new Popup<>().warning(Res.get("shared.notEnoughFunds", + btcFormatter.formatCoinWithCode(dataModel.totalToPayAsCoinProperty().get()), + btcFormatter.formatCoinWithCode(dataModel.getTotalAvailableBalance()))) + .actionButtonTextWithGoTo("navigation.funds.depositFunds") + .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) + .show(); + return false; + } + + } + + public void setIsCurrencyForMakerFeeBtc(boolean isCurrencyForMakerFeeBtc) { + dataModel.setPreferredCurrencyForMakerFeeBtc(isCurrencyForMakerFeeBtc); + applyMakerFee(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Handle focus + /////////////////////////////////////////////////////////////////////////////////////////// + + // On focus out we do validation and apply the data to the model + void onFocusOutAmountTextField(boolean oldValue, boolean newValue) { + if (oldValue && !newValue) { + InputValidator.ValidationResult result = isBtcInputValid(amount.get()); + amountValidationResult.set(result); + if (result.isValid) { + setAmountToModel(); + ignoreAmountStringListener = true; + amount.set(btcFormatter.formatCoin(dataModel.getAmount().get())); + ignoreAmountStringListener = false; + dataModel.calculateVolume(); + + if (!dataModel.isMinAmountLessOrEqualAmount()) + minAmount.set(amount.get()); + else + amountValidationResult.set(result); + + if (minAmount.get() != null) + minAmountValidationResult.set(isBtcInputValid(minAmount.get())); + } + } + } + + public void onFocusOutMinAmountTextField(boolean oldValue, boolean newValue) { + if (oldValue && !newValue) { + InputValidator.ValidationResult result = isBtcInputValid(minAmount.get()); + minAmountValidationResult.set(result); + if (result.isValid) { + syncMinAmountWithAmount = dataModel.getMinAmount().getValue().equals(dataModel.getAmount().getValue()); + setMinAmountToModel(); + minAmount.set(btcFormatter.formatCoin(dataModel.getMinAmount().get())); + + if (!dataModel.isMinAmountLessOrEqualAmount()) { + amount.set(minAmount.get()); + } else { + minAmountValidationResult.set(result); + if (amount.get() != null) + amountValidationResult.set(isBtcInputValid(amount.get())); + } + } else { + syncMinAmountWithAmount = true; + } + } + } + + void onFocusOutPriceTextField(boolean oldValue, boolean newValue) { + if (oldValue && !newValue) { + InputValidator.ValidationResult result = isPriceInputValid(price.get()); + boolean isValid = result.isValid; + priceValidationResult.set(result); + if (isValid) { + setPriceToModel(); + ignorePriceStringListener = true; + if (dataModel.getPrice().get() != null) + price.set(btcFormatter.formatPrice(dataModel.getPrice().get())); + ignorePriceStringListener = false; + dataModel.calculateVolume(); + dataModel.calculateAmount(); + applyMakerFee(); + } + } + } + + public void onFocusOutPriceAsPercentageTextField(boolean oldValue, boolean newValue) { + inputIsMarketBasedPrice = !oldValue && newValue; + if (oldValue && !newValue) + if (marketPriceMargin.get() == null) { + // field wasn't set manually + inputIsMarketBasedPrice = true; + } + marketPriceMargin.set(btcFormatter.formatRoundedDoubleWithPrecision(dataModel.getMarketPriceMargin() * 100, 2)); + } + + void onFocusOutVolumeTextField(boolean oldValue, boolean newValue) { + if (oldValue && !newValue) { + InputValidator.ValidationResult result = isVolumeInputValid(volume.get()); + volumeValidationResult.set(result); + if (result.isValid) { + setVolumeToModel(); + ignoreVolumeStringListener = true; + if (dataModel.getVolume().get() != null) + volume.set(btcFormatter.formatVolume(dataModel.getVolume().get())); + ignoreVolumeStringListener = false; + + dataModel.calculateAmount(); + + if (!dataModel.isMinAmountLessOrEqualAmount()) { + minAmount.set(amount.getValue()); + } else { + if (amount.get() != null) + amountValidationResult.set(isBtcInputValid(amount.get())); + + // We only check minAmountValidationResult if amountValidationResult is valid, otherwise we would get + // triggered a close of the popup when the minAmountValidationResult is applied + if (amountValidationResult.getValue() != null && amountValidationResult.getValue().isValid && minAmount.get() != null) + minAmountValidationResult.set(isBtcInputValid(minAmount.get())); + } + } + } + } + + void onFocusOutBuyerSecurityDepositTextField(boolean oldValue, boolean newValue) { + if (oldValue && !newValue) { + InputValidator.ValidationResult result = securityDepositValidator.validate(buyerSecurityDeposit.get()); + buyerSecurityDepositValidationResult.set(result); + if (result.isValid) { + Coin defaultSecurityDeposit = Restrictions.getDefaultBuyerSecurityDeposit(); + String key = "buyerSecurityDepositLowerAsDefault"; + if (preferences.showAgain(key) && + btcFormatter.parseToCoin(buyerSecurityDeposit.get()).compareTo(defaultSecurityDeposit) < 0) { + final String postfix = dataModel.isBuyOffer() ? + Res.get("createOffer.tooLowSecDeposit.makerIsBuyer") : + Res.get("createOffer.tooLowSecDeposit.makerIsSeller"); + new Popup<>() + .warning(Res.get("createOffer.tooLowSecDeposit.warning", + btcFormatter.formatCoinWithCode(defaultSecurityDeposit)) + "\n\n" + postfix) + .width(800) + .actionButtonText(Res.get("createOffer.resetToDefault")) + .onAction(() -> { + dataModel.setBuyerSecurityDeposit(defaultSecurityDeposit); + ignoreSecurityDepositStringListener = true; + buyerSecurityDeposit.set(btcFormatter.formatCoin(dataModel.getBuyerSecurityDeposit().get())); + ignoreSecurityDepositStringListener = false; + }) + .closeButtonText(Res.get("createOffer.useLowerValue")) + .onClose(this::applyBuyerSecurityDepositOnFocusOut) + .dontShowAgainId(key) + .show(); + } else { + applyBuyerSecurityDepositOnFocusOut(); + } + } + } + } + + private void applyBuyerSecurityDepositOnFocusOut() { + setBuyerSecurityDepositToModel(); + ignoreSecurityDepositStringListener = true; + buyerSecurityDeposit.set(btcFormatter.formatCoin(dataModel.getBuyerSecurityDeposit().get())); + ignoreSecurityDepositStringListener = false; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Getters + /////////////////////////////////////////////////////////////////////////////////////////// + + public boolean isPriceInRange() { + if (marketPriceMargin.get() != null && !marketPriceMargin.get().isEmpty()) { + if (Math.abs(btcFormatter.parsePercentStringToDouble(marketPriceMargin.get())) > preferences.getMaxPriceDistanceInPercent()) { + displayPriceOutOfRangePopup(); + return false; + } else { + return true; + } + } else { + return true; + } + } + + private void displayPriceOutOfRangePopup() { + Popup popup = new Popup<>(); + //noinspection unchecked + popup.warning(Res.get("createOffer.priceOutSideOfDeviation", + btcFormatter.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent()))) + .actionButtonText(Res.get("createOffer.changePrice")) + .onAction(popup::hide) + .closeButtonTextWithGoTo("navigation.settings.preferences") + .onClose(() -> navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class)) + .show(); + } + + BSFormatter getBtcFormatter() { + return btcFormatter; + } + + public boolean isSellOffer() { + return dataModel.getDirection() == OfferPayload.Direction.SELL; + } + + public TradeCurrency getTradeCurrency() { + return dataModel.getTradeCurrency(); + } + + public String getTradeAmount() { + return btcFormatter.formatCoinWithCode(dataModel.getAmount().get()); + } + + public String getSecurityDepositInfo() { + return btcFormatter.formatCoinWithCode(dataModel.getSecurityDeposit()) + + GUIUtil.getPercentageOfTradeAmount(dataModel.getSecurityDeposit(), dataModel.getAmount().get(), btcFormatter); + } + + public String getSecurityDepositWithCode() { + return btcFormatter.formatCoinWithCode(dataModel.getSecurityDeposit()); + } + + public String getMakerFee() { + //TODO use last bisq market price to estimate BSQ val + final Coin makerFeeAsCoin = dataModel.getMakerFee(); + final String makerFee = getFormatterForMakerFee().formatCoinWithCode(makerFeeAsCoin); + if (dataModel.isCurrencyForMakerFeeBtc()) + return makerFee + GUIUtil.getPercentageOfTradeAmount(makerFeeAsCoin, dataModel.getAmount().get(), btcFormatter); + else + return makerFee + " (" + Res.get("shared.tradingFeeInBsqInfo", btcFormatter.formatCoinWithCode(makerFeeAsCoin)) + ")"; + } + + public String getMakerFeePercentage() { + final Coin makerFeeAsCoin = dataModel.getMakerFee(); + if (dataModel.isCurrencyForMakerFeeBtc()) + return GUIUtil.getPercentage(makerFeeAsCoin, dataModel.getAmount().get(), btcFormatter); + else + return Res.get("shared.paidWithBsq"); + } + + public String getTotalToPayInfo() { + final String totalToPay = this.totalToPay.get(); + if (dataModel.isCurrencyForMakerFeeBtc()) + return totalToPay; + else + return totalToPay + " + " + bsqFormatter.formatCoinWithCode(dataModel.getMakerFee()); + } + + public String getTxFee() { + Coin txFeeAsCoin = dataModel.getTxFee(); + return btcFormatter.formatCoinWithCode(txFeeAsCoin) + + GUIUtil.getPercentageOfTradeAmount(txFeeAsCoin, dataModel.getAmount().get(), btcFormatter); + + } + + public String getTxFeePercentage() { + Coin txFeeAsCoin = dataModel.getTxFee(); + return GUIUtil.getPercentage(txFeeAsCoin, dataModel.getAmount().get(), btcFormatter); + } + + public PaymentAccount getPaymentAccount() { + return dataModel.getPaymentAccount(); + } + + public String getAmountDescription() { + return amountDescription; + } + + public String getDirectionLabel() { + return directionLabel; + } + + public String getAddressAsString() { + return addressAsString; + } + + public String getPaymentLabel() { + return paymentLabel; + } + + public String formatCoin(Coin coin) { + return btcFormatter.formatCoin(coin); + } + + public Offer createAndGetOffer() { + offer = dataModel.createAndGetOffer(); + return offer; + } + + boolean hasAcceptedArbitrators() { + return dataModel.hasAcceptedArbitrators(); + } + + boolean isReadyForTxBroadcast() { + return GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup); + } + + void showNotReadyForTxBroadcastPopups() { + GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); + } + + public M getDataModel() { + return dataModel; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private void setAmountToModel() { + if (amount.get() != null && !amount.get().isEmpty()) { + dataModel.setAmount(btcFormatter.parseToCoinWith4Decimals(amount.get())); + if (syncMinAmountWithAmount || dataModel.getMinAmount().get() == null || dataModel.getMinAmount().get().equals(Coin.ZERO)) { + minAmount.set(amount.get()); + setMinAmountToModel(); + } + } else { + dataModel.setAmount(null); + } + } + + private void setMinAmountToModel() { + if (minAmount.get() != null && !minAmount.get().isEmpty()) + dataModel.setMinAmount(btcFormatter.parseToCoinWith4Decimals(minAmount.get())); + else + dataModel.setMinAmount(null); + } + + private void setPriceToModel() { + if (price.get() != null && !price.get().isEmpty()) { + try { + dataModel.setPrice(Price.parse(dataModel.getTradeCurrencyCode().get(), this.price.get())); + } catch (Throwable t) { + log.debug(t.getMessage()); + } + } else { + dataModel.setPrice(null); + } + } + + private void setVolumeToModel() { + if (volume.get() != null && !volume.get().isEmpty()) { + try { + dataModel.setVolume(Volume.parse(volume.get(), dataModel.getTradeCurrencyCode().get())); + } catch (Throwable t) { + log.debug(t.getMessage()); + } + } else { + dataModel.setVolume(null); + } + } + + private void setBuyerSecurityDepositToModel() { + if (buyerSecurityDeposit.get() != null && !buyerSecurityDeposit.get().isEmpty()) { + dataModel.setBuyerSecurityDeposit(btcFormatter.parseToCoinWith4Decimals(buyerSecurityDeposit.get())); + } else { + dataModel.setBuyerSecurityDeposit(null); + } + } + + + private InputValidator.ValidationResult isBtcInputValid(String input) { + return btcValidator.validate(input); + } + + private InputValidator.ValidationResult isPriceInputValid(String input) { + return getPriceValidator().validate(input); + } + + private InputValidator.ValidationResult isVolumeInputValid(String input) { + return getVolumeValidator().validate(input); + } + + private MonetaryValidator getPriceValidator() { + return CurrencyUtil.isCryptoCurrency(getTradeCurrency().getCode()) ? altcoinValidator : fiatPriceValidator; + } + + private MonetaryValidator getVolumeValidator() { + final String code = getTradeCurrency().getCode(); + if (CurrencyUtil.isCryptoCurrency(code)) { + return code.equals("BSQ") ? bsqValidator : altcoinValidator; + } else { + return fiatVolumeValidator; + } + } + + private void updateSpinnerInfo() { + if (!showPayFundsScreenDisplayed.get() || + errorMessage.get() != null || + showTransactionPublishedScreen.get()) { + waitingForFundsText.set(""); + } else if (dataModel.getIsBtcWalletFunded().get()) { + waitingForFundsText.set(""); + /* if (dataModel.isFeeFromFundingTxSufficient.get()) { + spinnerInfoText.set(""); + } else { + spinnerInfoText.set("Check if funding tx miner fee is sufficient..."); + }*/ + } else { + waitingForFundsText.set(Res.get("shared.waitingForFunds")); + } + + isWaitingForFunds.set(!waitingForFundsText.get().isEmpty()); + } + + private void updateButtonDisableState() { + log.debug("updateButtonDisableState"); + boolean inputDataValid = isBtcInputValid(amount.get()).isValid && + isBtcInputValid(minAmount.get()).isValid && + isPriceInputValid(price.get()).isValid && + securityDepositValidator.validate(buyerSecurityDeposit.get()).isValid && + dataModel.getPrice().get() != null && + dataModel.getPrice().get().getValue() != 0 && + isVolumeInputValid(volume.get()).isValid && + dataModel.isMinAmountLessOrEqualAmount(); + + isNextButtonDisabled.set(!inputDataValid); + // boolean notSufficientFees = dataModel.isWalletFunded.get() && dataModel.isMainNet.get() && !dataModel.isFeeFromFundingTxSufficient.get(); + //isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || notSufficientFees); + isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || !dataModel.getIsBtcWalletFunded().get()); + } + + private void stopTimeoutTimer() { + if (timeoutTimer != null) { + timeoutTimer.stop(); + timeoutTimer = null; + } + } + + private BSFormatter getFormatterForMakerFee() { + return dataModel.isCurrencyForMakerFeeBtc() ? btcFormatter : bsqFormatter; + } +} diff --git a/src/main/java/bisq/desktop/main/offer/OfferView.java b/src/main/java/bisq/desktop/main/offer/OfferView.java index cd9a5f4874f..77a8ea61180 100644 --- a/src/main/java/bisq/desktop/main/offer/OfferView.java +++ b/src/main/java/bisq/desktop/main/offer/OfferView.java @@ -53,10 +53,9 @@ public abstract class OfferView extends ActivatableView { private OfferBookView offerBookView; private CreateOfferView createOfferView; private TakeOfferView takeOfferView; - private AnchorPane createOfferPane; + private AnchorPane createOfferPane, takeOfferPane; private Tab takeOfferTab, createOfferTab, offerBookTab; - private AnchorPane takeOfferPane; private final ViewLoader viewLoader; private final Navigation navigation; private final Preferences preferences; diff --git a/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java b/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java index a1d33363ad6..319c2d37567 100644 --- a/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java +++ b/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java @@ -17,779 +17,35 @@ package bisq.desktop.main.offer.createoffer; -import bisq.desktop.main.offer.OfferDataModel; +import bisq.desktop.main.offer.EditableOfferDataModel; 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; - /** * Domain for that UI element. * Note that the create offer domain has a deeper scope in the application domain (TradeManager). * That model is just responsible for the domain specific parts displayed needed in that UI element. */ -class CreateOfferDataModel extends OfferDataModel implements BsqBalanceListener { - private final OpenOfferManager openOfferManager; - private final BsqWalletService bsqWalletService; - private final Preferences preferences; - private final User user; - private final KeyRing keyRing; - private final P2PService p2PService; - private 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(); - - 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 - /////////////////////////////////////////////////////////////////////////////////////////// +class CreateOfferDataModel extends EditableOfferDataModel { @Inject - CreateOfferDataModel(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 - protected 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() - 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 = marketPriceAvailable && useMarketBasedPrice.get(); - 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(); - } - } - - 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; - } - - public TradeCurrency getTradeCurrency() { - return tradeCurrency; - } - - public PaymentAccount getPaymentAccount() { - return paymentAccount; - } - - boolean hasAcceptedArbitrators() { - final List acceptedArbitrators = user.getAcceptedArbitrators(); - return acceptedArbitrators != null && acceptedArbitrators.size() > 0; - } - - public 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; - } - - 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())); - } - - void setAmount(Coin amount) { - this.amount.set(amount); - } - - public void setPrice(Price price) { - this.price.set(price); - } - - public void setVolume(Volume volume) { - this.volume.set(volume); - } - - void setBuyerSecurityDeposit(Coin buyerSecurityDeposit) { - this.buyerSecurityDeposit.set(buyerSecurityDeposit); - preferences.setBuyerSecurityDepositAsLong(buyerSecurityDeposit.value); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Getters - /////////////////////////////////////////////////////////////////////////////////////////// - - ReadOnlyObjectProperty getAmount() { - return amount; - } - - ReadOnlyObjectProperty getMinAmount() { - return minAmount; - } - - ReadOnlyObjectProperty getPrice() { - return price; - } - - ReadOnlyObjectProperty getVolume() { - return volume; - } - - 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); + public CreateOfferDataModel(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(openOfferManager, btcWalletService, bsqWalletService, preferences, user, keyRing, p2PService, priceFeedService, filterManager, accountAgeWitnessService, tradeWalletService, feeService, formatter); } } diff --git a/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferView.java b/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferView.java index 27add9a7341..ffba4296afe 100644 --- a/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferView.java +++ b/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferView.java @@ -18,1279 +18,23 @@ package bisq.desktop.main.offer.createoffer; import bisq.desktop.Navigation; -import bisq.desktop.common.view.ActivatableViewAndModel; import bisq.desktop.common.view.FxmlView; -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.offer.OfferView; -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.offer.EditableOfferView; 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 javax.inject.Inject; - -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; - - - -import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import com.google.inject.Inject; @FxmlView -public class CreateOfferView extends ActivatableViewAndModel { - - private 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; - private GridPane gridPane; - private TitledGroupBg payFundsTitledGroupBg, setDepositTitledGroupBg; - 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; - 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; - - private int gridRow = 0; - private final List editOfferElements = new ArrayList<>(); - private boolean clearXchangeWarningDisplayed, isActivated; - private ChangeListener getShowWalletFundedNotificationListener; - private InfoInputTextField marketBasedPriceInfoInputTextField; - - /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor, lifecycle - /////////////////////////////////////////////////////////////////////////////////////////// +public class CreateOfferView extends EditableOfferView { @Inject - private CreateOfferView(CreateOfferViewModel 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.dataModel.isTabSelected) - doActivate(); - } - - private 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.dataModel.getPaymentAccounts()); - paymentAccountsComboBox.getSelectionModel().select(model.getPaymentAccount()); - - onPaymentAccountsComboBoxSelected(); - - balanceTextField.setTargetAmount(model.dataModel.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.dataModel.isTabSelected) - doActivate(); - else - deactivate(); - - isActivated = isSelected; - model.dataModel.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.dataModel.isMakerFeeValid() && model.dataModel.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.dataModel.getBalance().get().isPositive() && !model.placeOfferCompleted.get()) { - model.dataModel.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.dataModel.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.dataModel.getMakerFee(false); - String message = null; - if (makerFee != null) { - message = Res.get("popup.warning.insufficientBsqFundsForBtcFeePayment", - bsqFormatter.formatCoinWithCode(makerFee.subtract(model.dataModel.getBsqBalance()))); - - } else if (model.dataModel.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.dataModel.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()); - 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.dataModel.getTradeCurrency()); - } - } else { - currencyComboBox.setVisible(false); - currencyTextField.setText(""); - } - - currencyComboBox.setOnAction(currencyComboBoxSelectionHandler); - } - - private void onCurrencyComboBoxSelected() { - model.onCurrencySelected(currencyComboBox.getSelectionModel().getSelectedItem()); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Navigation - /////////////////////////////////////////////////////////////////////////////////////////// - - private 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.dataModel.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.dataModel.getIsBtcWalletFunded().not().and(model.showPayFundsScreenDisplayed)); - fundingHBox.managedProperty().bind(model.dataModel.getIsBtcWalletFunded().not().and(model.showPayFundsScreenDisplayed)); - waitingForFundsLabel.textProperty().bind(model.waitingForFundsText); - placeOfferButton.visibleProperty().bind(model.dataModel.getIsBtcWalletFunded().and(model.showPayFundsScreenDisplayed)); - placeOfferButton.managedProperty().bind(model.dataModel.getIsBtcWalletFunded().and(model.showPayFundsScreenDisplayed)); - placeOfferButton.disableProperty().bind(model.isPlaceOfferButtonDisabled); - cancelButton2.disableProperty().bind(model.cancelButtonDisabled); - - // trading account - currencyComboBox.prefWidthProperty().bind(paymentAccountsComboBox.widthProperty()); - currencyComboBox.managedProperty().bind(currencyComboBox.visibleProperty()); - currencyComboBoxLabel.visibleProperty().bind(currencyComboBox.visibleProperty()); - currencyComboBoxLabel.managedProperty().bind(currencyComboBox.visibleProperty()); - currencyTextField.visibleProperty().bind(currencyComboBox.visibleProperty().not()); - currencyTextField.managedProperty().bind(currencyComboBox.visibleProperty().not()); - currencyTextFieldLabel.visibleProperty().bind(currencyComboBox.visibleProperty().not()); - currencyTextFieldLabel.managedProperty().bind(currencyComboBox.visibleProperty().not()); - } - - 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 - currencyComboBox.managedProperty().unbind(); - currencyComboBox.prefWidthProperty().unbind(); - currencyComboBoxLabel.visibleProperty().unbind(); - currencyComboBoxLabel.managedProperty().unbind(); - currencyTextField.visibleProperty().unbind(); - currencyTextField.managedProperty().unbind(); - currencyTextFieldLabel.visibleProperty().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.dataModel.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.dataModel.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; - } - - private 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.dataModel.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.dataModel.getShowWalletFundedNotification().addListener(getShowWalletFundedNotificationListener); - - // warnings - model.errorMessage.addListener(errorMessageListener); - // model.dataModel.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.dataModel.getShowWalletFundedNotification().removeListener(getShowWalletFundedNotificationListener); - - // warnings - model.errorMessage.removeListener(errorMessageListener); - // model.dataModel.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() { - TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, gridRow, 2, Res.get("shared.selectTradingAccount")); - GridPane.setColumnSpan(titledGroupBg, 3); - - //noinspection unchecked - paymentAccountsComboBox = addLabelComboBox(gridPane, gridRow, Res.getWithCol("shared.tradingAccount"), Layout.FIRST_ROW_DISTANCE).second; - paymentAccountsComboBox.setPromptText(Res.get("shared.selectTradingAccount")); - paymentAccountsComboBox.setMinWidth(300); - editOfferElements.add(paymentAccountsComboBox); - - // we display either currencyComboBox (multi currency account) or currencyTextField (single) - Tuple2 currencyComboBoxTuple = addLabelComboBox(gridPane, ++gridRow, Res.getWithCol("shared.currency")); - currencyComboBoxLabel = currencyComboBoxTuple.first; - editOfferElements.add(currencyComboBoxLabel); - //noinspection unchecked - currencyComboBox = currencyComboBoxTuple.second; - editOfferElements.add(currencyComboBox); - currencyComboBox.setPromptText(Res.get("list.currency.select")); - currencyComboBox.setConverter(new StringConverter() { - @Override - public String toString(TradeCurrency tradeCurrency) { - return tradeCurrency.getNameAndCode(); - } - - @Override - public TradeCurrency fromString(String s) { - return null; - } - }); - - Tuple2 currencyTextFieldTuple = addLabelTextField(gridPane, gridRow, Res.getWithCol("shared.currency"), "", 5); - currencyTextFieldLabel = currencyTextFieldTuple.first; - editOfferElements.add(currencyTextFieldLabel); - currencyTextField = currencyTextFieldTuple.second; - editOfferElements.add(currencyTextField); - } - - private void addAmountPriceGroup() { - TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, Res.get("createOffer.setAmountPrice"), Layout.GROUP_DISTANCE); - GridPane.setColumnSpan(titledGroupBg, 3); - - imageView = new ImageView(); - imageView.setPickOnBounds(true); - directionLabel = new AutoTooltipLabel(); - directionLabel.setAlignment(Pos.CENTER); - directionLabel.setPadding(new Insets(-5, 0, 0, 0)); - directionLabel.setId("direction-icon-label"); - VBox imageVBox = new VBox(); - imageVBox.setAlignment(Pos.CENTER); - imageVBox.setSpacing(12); - imageVBox.getChildren().addAll(imageView, directionLabel); - GridPane.setRowIndex(imageVBox, gridRow); - GridPane.setRowSpan(imageVBox, 2); - GridPane.setMargin(imageVBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 10, 10, 10)); - gridPane.getChildren().add(imageVBox); - - addAmountPriceFields(); - addSecondRow(); - } - - private void addOptionsGroup() { - setDepositTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, Res.get("createOffer.setDeposit"), Layout.GROUP_DISTANCE); - - addBuyerSecurityDepositRow(); - - Tuple2 tuple = add2ButtonsAfterGroup(gridPane, ++gridRow, - Res.get("shared.nextStep"), Res.get("shared.cancel")); - nextButton = tuple.first; - editOfferElements.add(nextButton); - nextButton.disableProperty().bind(model.isNextButtonDisabled); - cancelButton1 = tuple.second; - editOfferElements.add(cancelButton1); - cancelButton1.setDefaultButton(false); - cancelButton1.setOnAction(e -> { - close(); - model.dataModel.swapTradeToSavings(); - }); - cancelButton1.setId("cancel-button"); - - GridPane.setMargin(nextButton, new Insets(-35, 0, 0, 0)); - nextButton.setOnAction(e -> { - if (model.isPriceInRange()) { - if (DevEnv.DAO_TRADING_ACTIVATED) - showFeeOption(); - else - onShowPayFundsScreen(); - } - }); - } - - private void showFeeOption() { - Coin makerFee = model.dataModel.getMakerFee(false); - String missingBsq = null; - if (makerFee != null) { - missingBsq = Res.get("popup.warning.insufficientBsqFundsForBtcFeePayment", - bsqFormatter.formatCoinWithCode(makerFee.subtract(model.dataModel.getBsqBalance()))); - - } else if (model.dataModel.getBsqBalance().isZero()) - missingBsq = Res.get("popup.warning.noBsqFundsForBtcFeePayment"); - - new FeeOptionWindow(model.makerFeeWithCode, - model.dataModel.isCurrencyForMakerFeeBtc(), - model.dataModel.isBsqForFeeAvailable(), - missingBsq, - navigation, - this::onShowPayFundsScreen) - .onSelectionChangedHandler(model::setIsCurrencyForMakerFeeBtc) - .onAction(this::onShowPayFundsScreen) - .hideCloseButton() - .show(); - } - - private void addBuyerSecurityDepositRow() { - final double top = model.dataModel.isBsqForFeeAvailable() ? 0 : Layout.FIRST_ROW_AND_GROUP_DISTANCE; - buyerSecurityDepositLabel = addLabel(gridPane, ++gridRow, - Res.getWithCol("shared.securityDepositBox.description", Res.get("shared.buyer")), - top); - - Tuple3 tuple = getEditableValueCurrencyBox( - Res.get("createOffer.securityDeposit.prompt")); - buyerSecurityDepositValueCurrencyBox = tuple.first; - buyerSecurityDepositInputTextField = tuple.second; - Label buyerSecurityDepositBtcLabel = tuple.third; - - editOfferElements.add(buyerSecurityDepositInputTextField); - editOfferElements.add(buyerSecurityDepositBtcLabel); - - GridPane.setRowIndex(buyerSecurityDepositValueCurrencyBox, gridRow); - GridPane.setColumnIndex(buyerSecurityDepositValueCurrencyBox, 1); - GridPane.setColumnSpan(buyerSecurityDepositValueCurrencyBox, 2); - GridPane.setMargin(buyerSecurityDepositValueCurrencyBox, new Insets(top, 0, 0, 0)); - gridPane.getChildren().add(buyerSecurityDepositValueCurrencyBox); - } - - private void addFundingGroup() { - // don't increase gridRow as we removed button when this gets visible - payFundsTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 3, - Res.get("createOffer.fundsBox.title"), Layout.GROUP_DISTANCE); - GridPane.setColumnSpan(payFundsTitledGroupBg, 3); - payFundsTitledGroupBg.setVisible(false); - - Tuple2 fundsTuple = addLabelFundsTextfield(gridPane, gridRow, - Res.get("shared.totalsNeeded"), Layout.FIRST_ROW_AND_GROUP_DISTANCE); - totalToPayLabel = fundsTuple.first; - totalToPayLabel.setVisible(false); - totalToPayTextField = fundsTuple.second; - totalToPayTextField.setVisible(false); - - qrCodeImageView = new ImageView(); - qrCodeImageView.setVisible(false); - qrCodeImageView.getStyleClass().add("qr-code"); - Tooltip.install(qrCodeImageView, new Tooltip(Res.get("shared.openLargeQRWindow"))); - qrCodeImageView.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute( - () -> UserThread.runAfter( - () -> new QRCodeWindow(getBitcoinURI()).show(), - 200, TimeUnit.MILLISECONDS))); - GridPane.setRowIndex(qrCodeImageView, gridRow); - GridPane.setColumnIndex(qrCodeImageView, 2); - GridPane.setRowSpan(qrCodeImageView, 3); - GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE - 9, 0, 0, 5)); - gridPane.getChildren().add(qrCodeImageView); - - Tuple2 addressTuple = addLabelAddressTextField(gridPane, ++gridRow, - Res.get("shared.tradeWalletAddress")); - addressLabel = addressTuple.first; - addressLabel.setVisible(false); - addressTextField = addressTuple.second; - addressTextField.setVisible(false); - - Tuple2 balanceTuple = addLabelBalanceTextField(gridPane, ++gridRow, - Res.get("shared.tradeWalletBalance")); - balanceLabel = balanceTuple.first; - balanceLabel.setVisible(false); - balanceTextField = balanceTuple.second; - balanceTextField.setVisible(false); - - fundingHBox = new HBox(); - fundingHBox.setVisible(false); - fundingHBox.setManaged(false); - fundingHBox.setSpacing(10); - Button fundFromSavingsWalletButton = new AutoTooltipButton(Res.get("shared.fundFromSavingsWalletButton")); - fundFromSavingsWalletButton.setDefaultButton(false); - fundFromSavingsWalletButton.setOnAction(e -> model.fundFromSavingsWallet()); - Label label = new AutoTooltipLabel(Res.get("shared.OR")); - label.setPadding(new Insets(5, 0, 0, 0)); - Button fundFromExternalWalletButton = new AutoTooltipButton(Res.get("shared.fundFromExternalWalletButton")); - fundFromExternalWalletButton.setDefaultButton(false); - fundFromExternalWalletButton.setOnAction(e -> GUIUtil.showFeeInfoBeforeExecute(this::openWallet)); - waitingForFundsBusyAnimation = new BusyAnimation(); - waitingForFundsLabel = new AutoTooltipLabel(); - waitingForFundsLabel.setPadding(new Insets(5, 0, 0, 0)); - - fundingHBox.getChildren().addAll(fundFromSavingsWalletButton, label, fundFromExternalWalletButton, waitingForFundsBusyAnimation, waitingForFundsLabel); - GridPane.setRowIndex(fundingHBox, ++gridRow); - GridPane.setColumnIndex(fundingHBox, 1); - GridPane.setMargin(fundingHBox, new Insets(15, 10, 0, 0)); - gridPane.getChildren().add(fundingHBox); - - - placeOfferButton = addButtonAfterGroup(gridPane, gridRow, ""); - placeOfferButton.setOnAction(e -> onPlaceOffer()); - placeOfferButton.setMinHeight(40); - placeOfferButton.setPadding(new Insets(0, 20, 0, 20)); - - cancelButton2 = addButton(gridPane, ++gridRow, Res.get("shared.cancel")); - cancelButton2.setOnAction(e -> { - if (model.dataModel.getIsBtcWalletFunded().get()) { - new Popup<>().warning(Res.get("createOffer.warnCancelOffer")) - .closeButtonText(Res.get("shared.no")) - .actionButtonText(Res.get("shared.yesCancel")) - .onAction(() -> { - close(); - model.dataModel.swapTradeToSavings(); - }) - .show(); - } else { - close(); - model.dataModel.swapTradeToSavings(); - } - }); - cancelButton2.setDefaultButton(false); - cancelButton2.setVisible(false); - } - - private void openWallet() { - try { - Utilities.openURI(URI.create(getBitcoinURI())); - } catch (Exception ex) { - log.warn(ex.getMessage()); - new Popup<>().warning(Res.get("shared.openDefaultWalletFailed")).show(); - } - } - - @NotNull - private String getBitcoinURI() { - return GUIUtil.getBitcoinURI(addressTextField.getAddress(), model.dataModel.getMissingCoin().get(), - model.getPaymentLabel()); - } - - private void addAmountPriceFields() { - // amountBox - Tuple3 amountValueCurrencyBoxTuple = getEditableValueCurrencyBox(Res.get("createOffer.amount.prompt")); - HBox amountValueCurrencyBox = amountValueCurrencyBoxTuple.first; - amountTextField = amountValueCurrencyBoxTuple.second; - editOfferElements.add(amountTextField); - Label amountBtcLabel = amountValueCurrencyBoxTuple.third; - editOfferElements.add(amountBtcLabel); - Tuple2 amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, model.getAmountDescription()); - amountDescriptionLabel = amountInputBoxTuple.first; - editOfferElements.add(amountDescriptionLabel); - VBox amountBox = amountInputBoxTuple.second; - - // x - xLabel = new AutoTooltipLabel(); - xLabel.setFont(Font.font("Helvetica-Bold", 20)); - xLabel.setPadding(new Insets(14, 3, 0, 3)); - xLabel.setMinWidth(14); - xLabel.setMaxWidth(14); - - // price as percent - Tuple3 priceAsPercentageTuple = getEditableValueCurrencyBoxWithInfo(Res.get("createOffer.price.prompt")); - - HBox priceAsPercentageValueCurrencyBox = priceAsPercentageTuple.first; - marketBasedPriceInfoInputTextField = priceAsPercentageTuple.second; - marketBasedPriceTextField = marketBasedPriceInfoInputTextField.getTextField(); - marketBasedPriceTextField.setPrefWidth(200); - editOfferElements.add(marketBasedPriceTextField); - marketBasedPriceLabel = priceAsPercentageTuple.third; - editOfferElements.add(marketBasedPriceLabel); - Tuple2 priceAsPercentageInputBoxTuple = getTradeInputBox(priceAsPercentageValueCurrencyBox, - Res.get("shared.distanceInPercent")); - percentagePriceDescription = priceAsPercentageInputBoxTuple.first; - percentagePriceDescription.setPrefWidth(200); - - getSmallIconForLabel(MaterialDesignIcon.CHART_LINE, percentagePriceDescription); - - percentagePriceBox = priceAsPercentageInputBoxTuple.second; - - // Fixed/Percentage toggle - priceTypeToggleButton = getIconButton(MaterialDesignIcon.SWAP_VERTICAL); - editOfferElements.add(priceTypeToggleButton); - - priceTypeToggleButton.setOnAction((actionEvent) -> { - updatePriceToggleButtons(model.dataModel.getUseMarketBasedPrice().getValue()); - }); - - // = - resultLabel = new AutoTooltipLabel("="); - resultLabel.setFont(Font.font("Helvetica-Bold", 20)); - resultLabel.setPadding(new Insets(14, 2, 0, 2)); - - // volume - Tuple3 volumeValueCurrencyBoxTuple = getEditableValueCurrencyBox(Res.get("createOffer.volume.prompt")); - HBox volumeValueCurrencyBox = volumeValueCurrencyBoxTuple.first; - volumeTextField = volumeValueCurrencyBoxTuple.second; - editOfferElements.add(volumeTextField); - volumeCurrencyLabel = volumeValueCurrencyBoxTuple.third; - editOfferElements.add(volumeCurrencyLabel); - Tuple2 volumeInputBoxTuple = getTradeInputBox(volumeValueCurrencyBox, model.volumeDescriptionLabel.get()); - volumeDescriptionLabel = volumeInputBoxTuple.first; - editOfferElements.add(volumeDescriptionLabel); - VBox volumeBox = volumeInputBoxTuple.second; - - firstRowHBox = new HBox(); - firstRowHBox.setSpacing(5); - firstRowHBox.setAlignment(Pos.CENTER_LEFT); - firstRowHBox.getChildren().addAll(amountBox, xLabel, percentagePriceBox, priceTypeToggleButton, resultLabel, volumeBox); - GridPane.setRowIndex(firstRowHBox, gridRow); - GridPane.setColumnIndex(firstRowHBox, 1); - GridPane.setMargin(firstRowHBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 10, 0, 0)); - GridPane.setColumnSpan(firstRowHBox, 2); - gridPane.getChildren().add(firstRowHBox); - } - - private void updatePriceToggleButtons(boolean fixedPriceSelected) { - int marketPriceAvailable = model.marketPriceAvailableProperty.get(); - fixedPriceSelected = fixedPriceSelected || (marketPriceAvailable == 0); - - if (marketPriceAvailable == 1) { - model.dataModel.setUseMarketBasedPrice(!fixedPriceSelected); - } - - percentagePriceBox.setDisable(fixedPriceSelected); - fixedPriceBox.setDisable(!fixedPriceSelected); - - if (fixedPriceSelected) { - if (firstRowHBox.getChildren().contains(percentagePriceBox)) - firstRowHBox.getChildren().remove(percentagePriceBox); - if (secondRowHBox.getChildren().contains(fixedPriceBox)) - secondRowHBox.getChildren().remove(fixedPriceBox); - if (!firstRowHBox.getChildren().contains(fixedPriceBox)) - firstRowHBox.getChildren().add(2, fixedPriceBox); - if (!secondRowHBox.getChildren().contains(percentagePriceBox)) - secondRowHBox.getChildren().add(2, percentagePriceBox); - } else { - if (firstRowHBox.getChildren().contains(fixedPriceBox)) - firstRowHBox.getChildren().remove(fixedPriceBox); - if (secondRowHBox.getChildren().contains(percentagePriceBox)) - secondRowHBox.getChildren().remove(percentagePriceBox); - if (!firstRowHBox.getChildren().contains(percentagePriceBox)) - firstRowHBox.getChildren().add(2, percentagePriceBox); - if (!secondRowHBox.getChildren().contains(fixedPriceBox)) - secondRowHBox.getChildren().add(2, fixedPriceBox); - } - } - - private void addSecondRow() { - // price as fiat - Tuple3 priceValueCurrencyBoxTuple = getEditableValueCurrencyBox( - Res.get("createOffer.price.prompt")); - HBox priceValueCurrencyBox = priceValueCurrencyBoxTuple.first; - fixedPriceTextField = priceValueCurrencyBoxTuple.second; - fixedPriceTextField.setPrefWidth(200); - editOfferElements.add(fixedPriceTextField); - priceCurrencyLabel = priceValueCurrencyBoxTuple.third; - editOfferElements.add(priceCurrencyLabel); - Tuple2 priceInputBoxTuple = getTradeInputBox(priceValueCurrencyBox, ""); - priceDescriptionLabel = priceInputBoxTuple.first; - - priceDescriptionLabel.setPrefWidth(200); - - getSmallIconForLabel(MaterialDesignIcon.LOCK, priceDescriptionLabel); - - editOfferElements.add(priceDescriptionLabel); - fixedPriceBox = priceInputBoxTuple.second; - - marketBasedPriceTextField.setPromptText(Res.get("shared.enterPercentageValue")); - marketBasedPriceLabel.setText("%"); - marketBasedPriceLabel.getStyleClass().add("percentage-label"); - - Tuple3 amountValueCurrencyBoxTuple = getEditableValueCurrencyBox( - Res.get("createOffer.amount.prompt")); - HBox amountValueCurrencyBox = amountValueCurrencyBoxTuple.first; - minAmountTextField = amountValueCurrencyBoxTuple.second; - editOfferElements.add(minAmountTextField); - Label minAmountBtcLabel = amountValueCurrencyBoxTuple.third; - editOfferElements.add(minAmountBtcLabel); - - Tuple2 amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, - Res.get("createOffer.amountPriceBox.minAmountDescription")); - - Label xLabel = new AutoTooltipLabel("x"); - xLabel.setFont(Font.font("Helvetica-Bold", 20)); - xLabel.setPadding(new Insets(14, 3, 0, 3)); - xLabel.setVisible(false); // we just use it to get the same layout as the upper row - - secondRowHBox = new HBox(); - secondRowHBox.setSpacing(5); - secondRowHBox.setAlignment(Pos.CENTER_LEFT); - secondRowHBox.getChildren().addAll(amountInputBoxTuple.second, xLabel, fixedPriceBox, priceTypeToggleButton); - GridPane.setRowIndex(secondRowHBox, ++gridRow); - GridPane.setColumnIndex(secondRowHBox, 1); - GridPane.setMargin(secondRowHBox, new Insets(0, 10, 0, 0)); - GridPane.setColumnSpan(secondRowHBox, 2); - gridPane.getChildren().add(secondRowHBox); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // PayInfo - /////////////////////////////////////////////////////////////////////////////////////////// - - private GridPane createInfoPopover() { - GridPane infoGridPane = new GridPane(); - infoGridPane.setHgap(5); - infoGridPane.setVgap(5); - infoGridPane.setPadding(new Insets(10, 10, 10, 10)); - - int i = 0; - if (model.isSellOffer()) - addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.tradeAmount"), model.tradeAmount.get()); - - addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.yourSecurityDeposit"), model.getSecurityDepositInfo()); - addPayInfoEntry(infoGridPane, i++, Res.get("createOffer.fundsBox.offerFee"), model.getMakerFee()); - addPayInfoEntry(infoGridPane, i++, Res.get("createOffer.fundsBox.networkFee"), model.getTxFee()); - Separator separator = new Separator(); - separator.setOrientation(Orientation.HORIZONTAL); - separator.getStyleClass().add("offer-separator"); - GridPane.setConstraints(separator, 1, i++); - infoGridPane.getChildren().add(separator); - addPayInfoEntry(infoGridPane, i, Res.getWithCol("shared.total"), model.getTotalToPayInfo()); - return infoGridPane; - } - - private void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) { - Label label = new AutoTooltipLabel(labelText); - TextField textField = new TextField(value); - textField.setMinWidth(500); - textField.setEditable(false); - textField.setFocusTraversable(false); - textField.setId("payment-info"); - GridPane.setConstraints(label, 0, row, 1, 1, HPos.RIGHT, VPos.CENTER); - GridPane.setConstraints(textField, 1, row); - infoGridPane.getChildren().addAll(label, textField); + public CreateOfferView(CreateOfferViewModel model, Navigation navigation, Preferences preferences, Transitions transitions, OfferDetailsWindow offerDetailsWindow, BSFormatter btcFormatter, BsqFormatter bsqFormatter) { + super(model, navigation, preferences, transitions, offerDetailsWindow, btcFormatter, bsqFormatter); } } diff --git a/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModel.java b/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModel.java index 781921b74f2..6df017d1d3e 100644 --- a/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModel.java +++ b/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModel.java @@ -18,1053 +18,29 @@ package bisq.desktop.main.offer.createoffer; import bisq.desktop.Navigation; -import bisq.desktop.common.model.ActivatableWithDataModel; import bisq.desktop.common.model.ViewModel; -import bisq.desktop.main.MainView; -import bisq.desktop.main.funds.FundsView; -import bisq.desktop.main.funds.deposit.DepositView; -import bisq.desktop.main.overlays.popups.Popup; -import bisq.desktop.main.settings.SettingsView; -import bisq.desktop.main.settings.preferences.PreferencesView; +import bisq.desktop.main.offer.EditableOfferViewModel; import bisq.desktop.util.BSFormatter; import bisq.desktop.util.BsqFormatter; -import bisq.desktop.util.GUIUtil; import bisq.desktop.util.validation.AltcoinValidator; import bisq.desktop.util.validation.BsqValidator; import bisq.desktop.util.validation.BtcValidator; import bisq.desktop.util.validation.FiatPriceValidator; import bisq.desktop.util.validation.FiatVolumeValidator; -import bisq.desktop.util.validation.MonetaryValidator; import bisq.desktop.util.validation.SecurityDepositValidator; -import bisq.core.app.BisqEnvironment; -import bisq.core.btc.Restrictions; import bisq.core.btc.wallet.WalletsSetup; -import bisq.core.locale.CurrencyUtil; -import bisq.core.locale.Res; -import bisq.core.locale.TradeCurrency; -import bisq.core.monetary.Altcoin; -import bisq.core.monetary.Price; -import bisq.core.monetary.Volume; -import bisq.core.offer.Offer; -import bisq.core.offer.OfferPayload; -import bisq.core.payment.PaymentAccount; -import bisq.core.provider.price.MarketPrice; import bisq.core.provider.price.PriceFeedService; import bisq.core.user.Preferences; -import bisq.core.util.validation.InputValidator; import bisq.network.p2p.P2PService; -import bisq.common.Timer; -import bisq.common.UserThread; -import bisq.common.app.DevEnv; -import bisq.common.util.MathUtils; +import com.google.inject.Inject; -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.utils.Fiat; - -import javax.inject.Inject; - -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.IntegerProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleIntegerProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; -import javafx.beans.value.ChangeListener; - -import java.util.concurrent.TimeUnit; - -import static javafx.beans.binding.Bindings.createStringBinding; - -class CreateOfferViewModel extends ActivatableWithDataModel implements ViewModel { - private final BtcValidator btcValidator; - private final BsqValidator bsqValidator; - private final SecurityDepositValidator securityDepositValidator; - private final P2PService p2PService; - private final WalletsSetup walletsSetup; - private final PriceFeedService priceFeedService; - private final Navigation navigation; - private final Preferences preferences; - private final BSFormatter btcFormatter; - private final BsqFormatter bsqFormatter; - private final FiatVolumeValidator fiatVolumeValidator; - private final FiatPriceValidator fiatPriceValidator; - private final AltcoinValidator altcoinValidator; - - private String amountDescription; - private String directionLabel; - private String addressAsString; - private final String paymentLabel; - private boolean createOfferRequested; - - final StringProperty amount = new SimpleStringProperty(); - final StringProperty minAmount = new SimpleStringProperty(); - final StringProperty buyerSecurityDeposit = new SimpleStringProperty(); - final String sellerSecurityDeposit; - - // Price in the viewModel is always dependent on fiat/altcoin: Fiat Fiat/BTC, for altcoins we use inverted price. - // The domain (dataModel) uses always the same price model (otherCurrencyBTC) - // If we would change the price representation in the domain we would not be backward compatible - final StringProperty price = new SimpleStringProperty(); - final StringProperty makerFee = new SimpleStringProperty(); - final StringProperty makerFeeWithCode = new SimpleStringProperty(); - final StringProperty makerFeeCurrencyCode = new SimpleStringProperty(); - - // Positive % value means always a better price form the maker's perspective: - // Buyer (with fiat): lower price as market - // Buyer (with altcoin): higher (display) price as market (display price is inverted) - final StringProperty marketPriceMargin = new SimpleStringProperty(); - final StringProperty volume = new SimpleStringProperty(); - final StringProperty volumeDescriptionLabel = new SimpleStringProperty(); - final StringProperty volumePromptLabel = new SimpleStringProperty(); - final StringProperty tradeAmount = new SimpleStringProperty(); - final StringProperty totalToPay = new SimpleStringProperty(); - final StringProperty errorMessage = new SimpleStringProperty(); - final StringProperty tradeCurrencyCode = new SimpleStringProperty(); - final StringProperty waitingForFundsText = new SimpleStringProperty(""); - - final BooleanProperty isPlaceOfferButtonDisabled = new SimpleBooleanProperty(true); - final BooleanProperty cancelButtonDisabled = new SimpleBooleanProperty(); - final BooleanProperty isNextButtonDisabled = new SimpleBooleanProperty(true); - final BooleanProperty placeOfferCompleted = new SimpleBooleanProperty(); - final BooleanProperty showPayFundsScreenDisplayed = new SimpleBooleanProperty(); - private final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); - final BooleanProperty isWaitingForFunds = new SimpleBooleanProperty(); - - final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(); - final ObjectProperty minAmountValidationResult = new SimpleObjectProperty<>(); - final ObjectProperty priceValidationResult = new SimpleObjectProperty<>(); - final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); - final ObjectProperty buyerSecurityDepositValidationResult = new SimpleObjectProperty<>(); - - // Those are needed for the addressTextField - private final ObjectProperty
address = new SimpleObjectProperty<>(); - - private ChangeListener amountStringListener; - private ChangeListener minAmountStringListener; - private ChangeListener priceStringListener, marketPriceMarginStringListener; - private ChangeListener volumeStringListener; - private ChangeListener securityDepositStringListener; - - private ChangeListener amountAsCoinListener; - private ChangeListener minAmountAsCoinListener; - private ChangeListener priceListener; - private ChangeListener volumeListener; - private ChangeListener securityDepositAsCoinListener; - - private ChangeListener isWalletFundedListener; - //private ChangeListener feeFromFundingTxListener; - private ChangeListener errorMessageListener; - private Offer offer; - private Timer timeoutTimer; - private boolean inputIsMarketBasedPrice; - private ChangeListener useMarketBasedPriceListener; - private boolean ignorePriceStringListener, ignoreVolumeStringListener, ignoreAmountStringListener, ignoreSecurityDepositStringListener; - private MarketPrice marketPrice; - final IntegerProperty marketPriceAvailableProperty = new SimpleIntegerProperty(-1); - private ChangeListener currenciesUpdateListener; - private boolean syncMinAmountWithAmount = true; - - /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor, lifecycle - /////////////////////////////////////////////////////////////////////////////////////////// +class CreateOfferViewModel extends EditableOfferViewModel implements ViewModel { @Inject - public CreateOfferViewModel(CreateOfferDataModel dataModel, - FiatVolumeValidator fiatVolumeValidator, - FiatPriceValidator fiatPriceValidator, - AltcoinValidator altcoinValidator, - BtcValidator btcValidator, - BsqValidator bsqValidator, - SecurityDepositValidator securityDepositValidator, - P2PService p2PService, - WalletsSetup walletsSetup, - PriceFeedService priceFeedService, - Navigation navigation, - Preferences preferences, - BSFormatter btcFormatter, - BsqFormatter bsqFormatter) { - super(dataModel); - - this.fiatVolumeValidator = fiatVolumeValidator; - this.fiatPriceValidator = fiatPriceValidator; - this.altcoinValidator = altcoinValidator; - this.btcValidator = btcValidator; - this.bsqValidator = bsqValidator; - this.securityDepositValidator = securityDepositValidator; - this.p2PService = p2PService; - this.walletsSetup = walletsSetup; - this.priceFeedService = priceFeedService; - this.navigation = navigation; - this.preferences = preferences; - this.btcFormatter = btcFormatter; - this.bsqFormatter = bsqFormatter; - - paymentLabel = Res.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId); - sellerSecurityDeposit = btcFormatter.formatCoin(dataModel.getSellerSecurityDeposit()); - - if (dataModel.getAddressEntry() != null) { - addressAsString = dataModel.getAddressEntry().getAddressString(); - address.set(dataModel.getAddressEntry().getAddress()); - } - createListeners(); - } - - @Override - protected void activate() { - if (DevEnv.isDevMode()) { - UserThread.runAfter(() -> { - switch (BisqEnvironment.getBaseCurrencyNetwork().getCurrencyCode()) { - case "BTC": - amount.set("0.0001"); - price.set("11029"); - break; - case "LTC": - amount.set("50"); - price.set("40"); - break; - case "DASH": - amount.set("0.1"); - price.set("40"); - break; - } - - minAmount.set(amount.get()); - onFocusOutPriceAsPercentageTextField(true, false); - applyMakerFee(); - updateButtonDisableState(); - setAmountToModel(); - setMinAmountToModel(); - setPriceToModel(); - dataModel.calculateVolume(); - dataModel.calculateTotalToPay(); - updateButtonDisableState(); - updateSpinnerInfo(); - }, 100, TimeUnit.MILLISECONDS); - } - - addBindings(); - addListeners(); - - updateButtonDisableState(); - - updateMarketPriceAvailable(); - } - - @Override - protected void deactivate() { - removeBindings(); - removeListeners(); - stopTimeoutTimer(); - } - - private void addBindings() { - if (dataModel.getDirection() == OfferPayload.Direction.BUY) { - volumeDescriptionLabel.bind(createStringBinding( - () -> Res.get("createOffer.amountPriceBox.buy.volumeDescription", dataModel.getTradeCurrencyCode().get()), - dataModel.getTradeCurrencyCode())); - } else { - volumeDescriptionLabel.bind(createStringBinding( - () -> Res.get("createOffer.amountPriceBox.sell.volumeDescription", dataModel.getTradeCurrencyCode().get()), - dataModel.getTradeCurrencyCode())); - } - volumePromptLabel.bind(createStringBinding( - () -> Res.get("createOffer.volume.prompt", dataModel.getTradeCurrencyCode().get()), - dataModel.getTradeCurrencyCode())); - - totalToPay.bind(createStringBinding(() -> btcFormatter.formatCoinWithCode(dataModel.totalToPayAsCoinProperty().get()), - dataModel.totalToPayAsCoinProperty())); - - - tradeAmount.bind(createStringBinding(() -> btcFormatter.formatCoinWithCode(dataModel.getAmount().get()), - dataModel.getAmount())); - - - tradeCurrencyCode.bind(dataModel.getTradeCurrencyCode()); - } - - private void removeBindings() { - totalToPay.unbind(); - tradeAmount.unbind(); - tradeCurrencyCode.unbind(); - volumeDescriptionLabel.unbind(); - volumePromptLabel.unbind(); - } - - private void createListeners() { - amountStringListener = (ov, oldValue, newValue) -> { - if (!ignoreAmountStringListener) { - if (isBtcInputValid(newValue).isValid) { - setAmountToModel(); - dataModel.calculateVolume(); - dataModel.calculateTotalToPay(); - } - updateButtonDisableState(); - } - }; - minAmountStringListener = (ov, oldValue, newValue) -> { - if (isBtcInputValid(newValue).isValid) - setMinAmountToModel(); - updateButtonDisableState(); - }; - priceStringListener = (ov, oldValue, newValue) -> { - updateMarketPriceAvailable(); - final String currencyCode = dataModel.getTradeCurrencyCode().get(); - if (!ignorePriceStringListener) { - if (isPriceInputValid(newValue).isValid) { - setPriceToModel(); - dataModel.calculateVolume(); - dataModel.calculateTotalToPay(); - - if (!inputIsMarketBasedPrice) { - if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { - double marketPriceAsDouble = marketPrice.getPrice(); - try { - double priceAsDouble = btcFormatter.parseNumberStringToDouble(price.get()); - double relation = priceAsDouble / marketPriceAsDouble; - final OfferPayload.Direction compareDirection = CurrencyUtil.isCryptoCurrency(currencyCode) ? - OfferPayload.Direction.SELL : - OfferPayload.Direction.BUY; - double percentage = dataModel.getDirection() == compareDirection ? 1 - relation : relation - 1; - percentage = MathUtils.roundDouble(percentage, 4); - dataModel.setMarketPriceMargin(percentage); - marketPriceMargin.set(btcFormatter.formatToPercent(percentage)); - applyMakerFee(); - } catch (NumberFormatException t) { - marketPriceMargin.set(""); - new Popup<>().warning(Res.get("validation.NaN")).show(); - } - } else { - log.debug("We don't have a market price. We use the static price instead."); - } - } - } - } - updateButtonDisableState(); - }; - marketPriceMarginStringListener = (ov, oldValue, newValue) -> { - if (inputIsMarketBasedPrice) { - try { - if (!newValue.isEmpty() && !newValue.equals("-")) { - double percentage = btcFormatter.parsePercentStringToDouble(newValue); - if (percentage >= 1 || percentage <= -1) { - new Popup<>().warning(Res.get("popup.warning.tooLargePercentageValue") + "\n" + - Res.get("popup.warning.examplePercentageValue")) - .show(); - } else { - final String currencyCode = dataModel.getTradeCurrencyCode().get(); - MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); - if (marketPrice != null && marketPrice.isRecentExternalPriceAvailable()) { - percentage = MathUtils.roundDouble(percentage, 4); - double marketPriceAsDouble = marketPrice.getPrice(); - final boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(currencyCode); - final OfferPayload.Direction compareDirection = isCryptoCurrency ? - OfferPayload.Direction.SELL : - OfferPayload.Direction.BUY; - double factor = dataModel.getDirection() == compareDirection ? - 1 - percentage : - 1 + percentage; - double targetPrice = marketPriceAsDouble * factor; - int precision = isCryptoCurrency ? - Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT; - // protect from triggering unwanted updates - ignorePriceStringListener = true; - price.set(btcFormatter.formatRoundedDoubleWithPrecision(targetPrice, precision)); - ignorePriceStringListener = false; - setPriceToModel(); - dataModel.setMarketPriceMargin(percentage); - dataModel.calculateVolume(); - dataModel.calculateTotalToPay(); - updateButtonDisableState(); - applyMakerFee(); - } else { - new Popup<>().warning(Res.get("popup.warning.noPriceFeedAvailable")).show(); - marketPriceMargin.set(""); - } - } - } - } catch (NumberFormatException t) { - log.error(t.toString()); - t.printStackTrace(); - new Popup<>().warning(Res.get("validation.NaN")).show(); - } catch (Throwable t) { - log.error(t.toString()); - t.printStackTrace(); - new Popup<>().warning(Res.get("validation.inputError", t.toString())).show(); - } - } - }; - useMarketBasedPriceListener = (observable, oldValue, newValue) -> { - if (newValue) - priceValidationResult.set(new InputValidator.ValidationResult(true)); - }; - - volumeStringListener = (ov, oldValue, newValue) -> { - if (!ignoreVolumeStringListener) { - if (isVolumeInputValid(newValue).isValid) { - setVolumeToModel(); - setPriceToModel(); - dataModel.calculateAmount(); - dataModel.calculateTotalToPay(); - } - updateButtonDisableState(); - } - }; - securityDepositStringListener = (ov, oldValue, newValue) -> { - if (!ignoreSecurityDepositStringListener) { - if (securityDepositValidator.validate(newValue).isValid) { - setBuyerSecurityDepositToModel(); - dataModel.calculateTotalToPay(); - } - updateButtonDisableState(); - } - }; - - - amountAsCoinListener = (ov, oldValue, newValue) -> { - if (newValue != null) - amount.set(btcFormatter.formatCoin(newValue)); - else - amount.set(""); - - applyMakerFee(); - }; - minAmountAsCoinListener = (ov, oldValue, newValue) -> { - if (newValue != null) - minAmount.set(btcFormatter.formatCoin(newValue)); - else - minAmount.set(""); - }; - priceListener = (ov, oldValue, newValue) -> { - ignorePriceStringListener = true; - if (newValue != null) - price.set(btcFormatter.formatPrice(newValue)); - else - price.set(""); - - ignorePriceStringListener = false; - applyMakerFee(); - }; - volumeListener = (ov, oldValue, newValue) -> { - ignoreVolumeStringListener = true; - if (newValue != null) - volume.set(btcFormatter.formatVolume(newValue)); - else - volume.set(""); - - ignoreVolumeStringListener = false; - applyMakerFee(); - }; - - securityDepositAsCoinListener = (ov, oldValue, newValue) -> { - if (newValue != null) - buyerSecurityDeposit.set(btcFormatter.formatCoin(newValue)); - else - buyerSecurityDeposit.set(""); - }; - - - isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); - /* feeFromFundingTxListener = (ov, oldValue, newValue) -> { - updateButtonDisableState(); - };*/ - - currenciesUpdateListener = (observable, oldValue, newValue) -> { - updateMarketPriceAvailable(); - updateButtonDisableState(); - }; - } - - private void applyMakerFee() { - makerFee.set(getFormatterForMakerFee().formatCoin(dataModel.getMakerFee())); - makerFeeWithCode.set(getFormatterForMakerFee().formatCoinWithCode(dataModel.getMakerFee())); - makerFeeCurrencyCode.set(dataModel.isCurrencyForMakerFeeBtc() ? Res.getBaseCurrencyCode() : "BSQ"); - } - - private void updateMarketPriceAvailable() { - marketPrice = priceFeedService.getMarketPrice(dataModel.getTradeCurrencyCode().get()); - marketPriceAvailableProperty.set(marketPrice == null || !marketPrice.isExternallyProvidedPrice() ? 0 : 1); - dataModel.setMarketPriceAvailable(marketPrice != null && marketPrice.isExternallyProvidedPrice()); - } - - private void addListeners() { - // Bidirectional bindings are used for all input fields: amount, price, volume and minAmount - // We do volume/amount calculation during input, so user has immediate feedback - amount.addListener(amountStringListener); - minAmount.addListener(minAmountStringListener); - price.addListener(priceStringListener); - marketPriceMargin.addListener(marketPriceMarginStringListener); - dataModel.getUseMarketBasedPrice().addListener(useMarketBasedPriceListener); - volume.addListener(volumeStringListener); - buyerSecurityDeposit.addListener(securityDepositStringListener); - - // Binding with Bindings.createObjectBinding does not work because of bi-directional binding - dataModel.getAmount().addListener(amountAsCoinListener); - dataModel.getMinAmount().addListener(minAmountAsCoinListener); - dataModel.getPrice().addListener(priceListener); - dataModel.getVolume().addListener(volumeListener); - dataModel.getBuyerSecurityDeposit().addListener(securityDepositAsCoinListener); - - // dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener); - dataModel.getIsBtcWalletFunded().addListener(isWalletFundedListener); - - priceFeedService.updateCounterProperty().addListener(currenciesUpdateListener); - } - - private void removeListeners() { - amount.removeListener(amountStringListener); - minAmount.removeListener(minAmountStringListener); - price.removeListener(priceStringListener); - marketPriceMargin.removeListener(marketPriceMarginStringListener); - dataModel.getUseMarketBasedPrice().removeListener(useMarketBasedPriceListener); - volume.removeListener(volumeStringListener); - buyerSecurityDeposit.removeListener(securityDepositStringListener); - - // Binding with Bindings.createObjectBinding does not work because of bi-directional binding - dataModel.getAmount().removeListener(amountAsCoinListener); - dataModel.getMinAmount().removeListener(minAmountAsCoinListener); - dataModel.getPrice().removeListener(priceListener); - dataModel.getVolume().removeListener(volumeListener); - dataModel.getBuyerSecurityDeposit().removeListener(securityDepositAsCoinListener); - - //dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); - dataModel.getIsBtcWalletFunded().removeListener(isWalletFundedListener); - - if (offer != null && errorMessageListener != null) - offer.getErrorMessageProperty().removeListener(errorMessageListener); - - priceFeedService.updateCounterProperty().removeListener(currenciesUpdateListener); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // API - /////////////////////////////////////////////////////////////////////////////////////////// - - boolean initWithData(OfferPayload.Direction direction, TradeCurrency tradeCurrency) { - boolean result = dataModel.initWithData(direction, tradeCurrency); - if (dataModel.paymentAccount != null) - btcValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin(dataModel.getTradeCurrencyCode().get())); - btcValidator.setMaxTradeLimit(Coin.valueOf(dataModel.getMaxTradeLimit())); - btcValidator.setMinValue(Restrictions.getMinTradeAmount()); - - final boolean isBuy = dataModel.getDirection() == OfferPayload.Direction.BUY; - directionLabel = isBuy ? Res.get("shared.buyBitcoin") : Res.get("shared.sellBitcoin"); - amountDescription = Res.get("createOffer.amountPriceBox.amountDescription", - isBuy ? Res.get("shared.buy") : Res.get("shared.sell")); - - buyerSecurityDeposit.set(btcFormatter.formatCoin(dataModel.getBuyerSecurityDeposit().get())); - - applyMakerFee(); - return result; - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // UI actions - /////////////////////////////////////////////////////////////////////////////////////////// - - void onPlaceOffer(Offer offer, Runnable resultHandler) { - errorMessage.set(null); - createOfferRequested = true; - - if (timeoutTimer == null) { - timeoutTimer = UserThread.runAfter(() -> { - stopTimeoutTimer(); - createOfferRequested = false; - errorMessage.set(Res.get("createOffer.timeoutAtPublishing")); - - updateButtonDisableState(); - updateSpinnerInfo(); - - resultHandler.run(); - }, 60); - } - errorMessageListener = (observable, oldValue, newValue) -> { - if (newValue != null) { - stopTimeoutTimer(); - createOfferRequested = false; - if (offer.getState() == Offer.State.OFFER_FEE_PAID) - errorMessage.set(newValue + Res.get("createOffer.errorInfo")); - else - errorMessage.set(newValue); - - updateButtonDisableState(); - updateSpinnerInfo(); - - resultHandler.run(); - } - }; - - offer.errorMessageProperty().addListener(errorMessageListener); - - dataModel.onPlaceOffer(offer, transaction -> { - stopTimeoutTimer(); - resultHandler.run(); - placeOfferCompleted.set(true); - errorMessage.set(null); - }); - - updateButtonDisableState(); - updateSpinnerInfo(); - } - - public void onPaymentAccountSelected(PaymentAccount paymentAccount) { - dataModel.onPaymentAccountSelected(paymentAccount); - if (amount.get() != null) - amountValidationResult.set(isBtcInputValid(amount.get())); - - btcValidator.setMaxValue(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin(dataModel.getTradeCurrencyCode().get())); - btcValidator.setMaxTradeLimit(Coin.valueOf(dataModel.getMaxTradeLimit())); - } - - public void onCurrencySelected(TradeCurrency tradeCurrency) { - dataModel.onCurrencySelected(tradeCurrency); - - marketPrice = priceFeedService.getMarketPrice(dataModel.getTradeCurrencyCode().get()); - marketPriceAvailableProperty.set(marketPrice == null || !marketPrice.isExternallyProvidedPrice() ? 0 : 1); - updateButtonDisableState(); - } - - void onShowPayFundsScreen() { - dataModel.estimateTxSize(); - dataModel.requestTxFee(); - showPayFundsScreenDisplayed.set(true); - updateSpinnerInfo(); - } - - boolean fundFromSavingsWallet() { - dataModel.fundFromSavingsWallet(); - if (dataModel.getIsBtcWalletFunded().get()) { - updateButtonDisableState(); - return true; - } else { - //noinspection unchecked - new Popup<>().warning(Res.get("shared.notEnoughFunds", - btcFormatter.formatCoinWithCode(dataModel.totalToPayAsCoinProperty().get()), - btcFormatter.formatCoinWithCode(dataModel.getTotalAvailableBalance()))) - .actionButtonTextWithGoTo("navigation.funds.depositFunds") - .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) - .show(); - return false; - } - - } - - void setIsCurrencyForMakerFeeBtc(boolean isCurrencyForMakerFeeBtc) { - dataModel.setPreferredCurrencyForMakerFeeBtc(isCurrencyForMakerFeeBtc); - applyMakerFee(); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Handle focus - /////////////////////////////////////////////////////////////////////////////////////////// - - // On focus out we do validation and apply the data to the model - void onFocusOutAmountTextField(boolean oldValue, boolean newValue) { - if (oldValue && !newValue) { - InputValidator.ValidationResult result = isBtcInputValid(amount.get()); - amountValidationResult.set(result); - if (result.isValid) { - setAmountToModel(); - ignoreAmountStringListener = true; - amount.set(btcFormatter.formatCoin(dataModel.getAmount().get())); - ignoreAmountStringListener = false; - dataModel.calculateVolume(); - - if (!dataModel.isMinAmountLessOrEqualAmount()) - minAmount.set(amount.get()); - else - amountValidationResult.set(result); - - if (minAmount.get() != null) - minAmountValidationResult.set(isBtcInputValid(minAmount.get())); - } - } - } - - void onFocusOutMinAmountTextField(boolean oldValue, boolean newValue) { - if (oldValue && !newValue) { - InputValidator.ValidationResult result = isBtcInputValid(minAmount.get()); - minAmountValidationResult.set(result); - if (result.isValid) { - syncMinAmountWithAmount = dataModel.getMinAmount().getValue().equals(dataModel.getAmount().getValue()); - setMinAmountToModel(); - minAmount.set(btcFormatter.formatCoin(dataModel.getMinAmount().get())); - - if (!dataModel.isMinAmountLessOrEqualAmount()) { - amount.set(minAmount.get()); - } else { - minAmountValidationResult.set(result); - if (amount.get() != null) - amountValidationResult.set(isBtcInputValid(amount.get())); - } - } else { - syncMinAmountWithAmount = true; - } - } - } - - void onFocusOutPriceTextField(boolean oldValue, boolean newValue) { - if (oldValue && !newValue) { - InputValidator.ValidationResult result = isPriceInputValid(price.get()); - boolean isValid = result.isValid; - priceValidationResult.set(result); - if (isValid) { - setPriceToModel(); - ignorePriceStringListener = true; - if (dataModel.getPrice().get() != null) - price.set(btcFormatter.formatPrice(dataModel.getPrice().get())); - ignorePriceStringListener = false; - dataModel.calculateVolume(); - dataModel.calculateAmount(); - applyMakerFee(); - } - } - } - - void onFocusOutPriceAsPercentageTextField(boolean oldValue, boolean newValue) { - inputIsMarketBasedPrice = !oldValue && newValue; - if (oldValue && !newValue) - if (marketPriceMargin.get() == null) { - // field wasn't set manually - inputIsMarketBasedPrice = true; - } - marketPriceMargin.set(btcFormatter.formatRoundedDoubleWithPrecision(dataModel.getMarketPriceMargin() * 100, 2)); - } - - void onFocusOutVolumeTextField(boolean oldValue, boolean newValue) { - if (oldValue && !newValue) { - InputValidator.ValidationResult result = isVolumeInputValid(volume.get()); - volumeValidationResult.set(result); - if (result.isValid) { - setVolumeToModel(); - ignoreVolumeStringListener = true; - if (dataModel.getVolume().get() != null) - volume.set(btcFormatter.formatVolume(dataModel.getVolume().get())); - ignoreVolumeStringListener = false; - - dataModel.calculateAmount(); - - if (!dataModel.isMinAmountLessOrEqualAmount()) { - minAmount.set(amount.getValue()); - } else { - if (amount.get() != null) - amountValidationResult.set(isBtcInputValid(amount.get())); - - // We only check minAmountValidationResult if amountValidationResult is valid, otherwise we would get - // triggered a close of the popup when the minAmountValidationResult is applied - if (amountValidationResult.getValue() != null && amountValidationResult.getValue().isValid && minAmount.get() != null) - minAmountValidationResult.set(isBtcInputValid(minAmount.get())); - } - } - } - } - - void onFocusOutBuyerSecurityDepositTextField(boolean oldValue, boolean newValue) { - if (oldValue && !newValue) { - InputValidator.ValidationResult result = securityDepositValidator.validate(buyerSecurityDeposit.get()); - buyerSecurityDepositValidationResult.set(result); - if (result.isValid) { - Coin defaultSecurityDeposit = Restrictions.getDefaultBuyerSecurityDeposit(); - String key = "buyerSecurityDepositLowerAsDefault"; - if (preferences.showAgain(key) && - btcFormatter.parseToCoin(buyerSecurityDeposit.get()).compareTo(defaultSecurityDeposit) < 0) { - final String postfix = dataModel.isBuyOffer() ? - Res.get("createOffer.tooLowSecDeposit.makerIsBuyer") : - Res.get("createOffer.tooLowSecDeposit.makerIsSeller"); - new Popup<>() - .warning(Res.get("createOffer.tooLowSecDeposit.warning", - btcFormatter.formatCoinWithCode(defaultSecurityDeposit)) + "\n\n" + postfix) - .width(800) - .actionButtonText(Res.get("createOffer.resetToDefault")) - .onAction(() -> { - dataModel.setBuyerSecurityDeposit(defaultSecurityDeposit); - ignoreSecurityDepositStringListener = true; - buyerSecurityDeposit.set(btcFormatter.formatCoin(dataModel.getBuyerSecurityDeposit().get())); - ignoreSecurityDepositStringListener = false; - }) - .closeButtonText(Res.get("createOffer.useLowerValue")) - .onClose(this::applyBuyerSecurityDepositOnFocusOut) - .dontShowAgainId(key) - .show(); - } else { - applyBuyerSecurityDepositOnFocusOut(); - } - } - } - } - - private void applyBuyerSecurityDepositOnFocusOut() { - setBuyerSecurityDepositToModel(); - ignoreSecurityDepositStringListener = true; - buyerSecurityDeposit.set(btcFormatter.formatCoin(dataModel.getBuyerSecurityDeposit().get())); - ignoreSecurityDepositStringListener = false; - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Getters - /////////////////////////////////////////////////////////////////////////////////////////// - - public boolean isPriceInRange() { - if (marketPriceMargin.get() != null && !marketPriceMargin.get().isEmpty()) { - if (Math.abs(btcFormatter.parsePercentStringToDouble(marketPriceMargin.get())) > preferences.getMaxPriceDistanceInPercent()) { - displayPriceOutOfRangePopup(); - return false; - } else { - return true; - } - } else { - return true; - } - } - - private void displayPriceOutOfRangePopup() { - Popup popup = new Popup<>(); - //noinspection unchecked - popup.warning(Res.get("createOffer.priceOutSideOfDeviation", - btcFormatter.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent()))) - .actionButtonText(Res.get("createOffer.changePrice")) - .onAction(popup::hide) - .closeButtonTextWithGoTo("navigation.settings.preferences") - .onClose(() -> navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class)) - .show(); - } - - BSFormatter getBtcFormatter() { - return btcFormatter; - } - - boolean isSellOffer() { - return dataModel.getDirection() == OfferPayload.Direction.SELL; - } - - public TradeCurrency getTradeCurrency() { - return dataModel.getTradeCurrency(); - } - - public String getTradeAmount() { - return btcFormatter.formatCoinWithCode(dataModel.getAmount().get()); - } - - public String getSecurityDepositInfo() { - return btcFormatter.formatCoinWithCode(dataModel.getSecurityDeposit()) + - GUIUtil.getPercentageOfTradeAmount(dataModel.getSecurityDeposit(), dataModel.getAmount().get(), btcFormatter); - } - - public String getSecurityDepositWithCode() { - return btcFormatter.formatCoinWithCode(dataModel.getSecurityDeposit()); - } - - public String getMakerFee() { - //TODO use last bisq market price to estimate BSQ val - final Coin makerFeeAsCoin = dataModel.getMakerFee(); - final String makerFee = getFormatterForMakerFee().formatCoinWithCode(makerFeeAsCoin); - if (dataModel.isCurrencyForMakerFeeBtc()) - return makerFee + GUIUtil.getPercentageOfTradeAmount(makerFeeAsCoin, dataModel.getAmount().get(), btcFormatter); - else - return makerFee + " (" + Res.get("shared.tradingFeeInBsqInfo", btcFormatter.formatCoinWithCode(makerFeeAsCoin)) + ")"; - } - - public String getMakerFeePercentage() { - final Coin makerFeeAsCoin = dataModel.getMakerFee(); - if (dataModel.isCurrencyForMakerFeeBtc()) - return GUIUtil.getPercentage(makerFeeAsCoin, dataModel.getAmount().get(), btcFormatter); - else - return Res.get("shared.paidWithBsq"); - } - - public String getTotalToPayInfo() { - final String totalToPay = this.totalToPay.get(); - if (dataModel.isCurrencyForMakerFeeBtc()) - return totalToPay; - else - return totalToPay + " + " + bsqFormatter.formatCoinWithCode(dataModel.getMakerFee()); - } - - public String getTxFee() { - Coin txFeeAsCoin = dataModel.getTxFee(); - return btcFormatter.formatCoinWithCode(txFeeAsCoin) + - GUIUtil.getPercentageOfTradeAmount(txFeeAsCoin, dataModel.getAmount().get(), btcFormatter); - - } - - public String getTxFeePercentage() { - Coin txFeeAsCoin = dataModel.getTxFee(); - return GUIUtil.getPercentage(txFeeAsCoin, dataModel.getAmount().get(), btcFormatter); - } - - public PaymentAccount getPaymentAccount() { - return dataModel.getPaymentAccount(); - } - - public String getAmountDescription() { - return amountDescription; - } - - public String getDirectionLabel() { - return directionLabel; - } - - public String getAddressAsString() { - return addressAsString; - } - - public String getPaymentLabel() { - return paymentLabel; - } - - public String formatCoin(Coin coin) { - return btcFormatter.formatCoin(coin); - } - - public Offer createAndGetOffer() { - offer = dataModel.createAndGetOffer(); - return offer; - } - - boolean hasAcceptedArbitrators() { - return dataModel.hasAcceptedArbitrators(); - } - - boolean isReadyForTxBroadcast() { - return GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup); - } - - void showNotReadyForTxBroadcastPopups() { - GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Utils - /////////////////////////////////////////////////////////////////////////////////////////// - - private void setAmountToModel() { - if (amount.get() != null && !amount.get().isEmpty()) { - dataModel.setAmount(btcFormatter.parseToCoinWith4Decimals(amount.get())); - if (syncMinAmountWithAmount || dataModel.getMinAmount().get() == null || dataModel.getMinAmount().get().equals(Coin.ZERO)) { - minAmount.set(amount.get()); - setMinAmountToModel(); - } - } else { - dataModel.setAmount(null); - } - } - - private void setMinAmountToModel() { - if (minAmount.get() != null && !minAmount.get().isEmpty()) - dataModel.setMinAmount(btcFormatter.parseToCoinWith4Decimals(minAmount.get())); - else - dataModel.setMinAmount(null); - } - - private void setPriceToModel() { - if (price.get() != null && !price.get().isEmpty()) { - try { - dataModel.setPrice(Price.parse(dataModel.getTradeCurrencyCode().get(), this.price.get())); - } catch (Throwable t) { - log.debug(t.getMessage()); - } - } else { - dataModel.setPrice(null); - } - } - - private void setVolumeToModel() { - if (volume.get() != null && !volume.get().isEmpty()) { - try { - dataModel.setVolume(Volume.parse(volume.get(), dataModel.getTradeCurrencyCode().get())); - } catch (Throwable t) { - log.debug(t.getMessage()); - } - } else { - dataModel.setVolume(null); - } - } - - private void setBuyerSecurityDepositToModel() { - if (buyerSecurityDeposit.get() != null && !buyerSecurityDeposit.get().isEmpty()) { - dataModel.setBuyerSecurityDeposit(btcFormatter.parseToCoinWith4Decimals(buyerSecurityDeposit.get())); - } else { - dataModel.setBuyerSecurityDeposit(null); - } - } - - - private InputValidator.ValidationResult isBtcInputValid(String input) { - return btcValidator.validate(input); - } - - private InputValidator.ValidationResult isPriceInputValid(String input) { - return getPriceValidator().validate(input); - } - - private InputValidator.ValidationResult isVolumeInputValid(String input) { - return getVolumeValidator().validate(input); - } - - private MonetaryValidator getPriceValidator() { - return CurrencyUtil.isCryptoCurrency(getTradeCurrency().getCode()) ? altcoinValidator : fiatPriceValidator; - } - - private MonetaryValidator getVolumeValidator() { - final String code = getTradeCurrency().getCode(); - if (CurrencyUtil.isCryptoCurrency(code)) { - return code.equals("BSQ") ? bsqValidator : altcoinValidator; - } else { - return fiatVolumeValidator; - } - } - - private void updateSpinnerInfo() { - if (!showPayFundsScreenDisplayed.get() || - errorMessage.get() != null || - showTransactionPublishedScreen.get()) { - waitingForFundsText.set(""); - } else if (dataModel.getIsBtcWalletFunded().get()) { - waitingForFundsText.set(""); - /* if (dataModel.isFeeFromFundingTxSufficient.get()) { - spinnerInfoText.set(""); - } else { - spinnerInfoText.set("Check if funding tx miner fee is sufficient..."); - }*/ - } else { - waitingForFundsText.set(Res.get("shared.waitingForFunds")); - } - - isWaitingForFunds.set(!waitingForFundsText.get().isEmpty()); - } - - private void updateButtonDisableState() { - log.debug("updateButtonDisableState"); - boolean inputDataValid = isBtcInputValid(amount.get()).isValid && - isBtcInputValid(minAmount.get()).isValid && - isPriceInputValid(price.get()).isValid && - securityDepositValidator.validate(buyerSecurityDeposit.get()).isValid && - dataModel.getPrice().get() != null && - dataModel.getPrice().get().getValue() != 0 && - isVolumeInputValid(volume.get()).isValid && - dataModel.isMinAmountLessOrEqualAmount(); - - isNextButtonDisabled.set(!inputDataValid); - // boolean notSufficientFees = dataModel.isWalletFunded.get() && dataModel.isMainNet.get() && !dataModel.isFeeFromFundingTxSufficient.get(); - //isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || notSufficientFees); - isPlaceOfferButtonDisabled.set(createOfferRequested || !inputDataValid || !dataModel.getIsBtcWalletFunded().get()); - } - - private void stopTimeoutTimer() { - if (timeoutTimer != null) { - timeoutTimer.stop(); - timeoutTimer = null; - } - } - - private BSFormatter getFormatterForMakerFee() { - return dataModel.isCurrencyForMakerFeeBtc() ? btcFormatter : bsqFormatter; + public CreateOfferViewModel(CreateOfferDataModel dataModel, FiatVolumeValidator fiatVolumeValidator, FiatPriceValidator fiatPriceValidator, AltcoinValidator altcoinValidator, BtcValidator btcValidator, BsqValidator bsqValidator, SecurityDepositValidator securityDepositValidator, P2PService p2PService, WalletsSetup walletsSetup, PriceFeedService priceFeedService, Navigation navigation, Preferences preferences, BSFormatter btcFormatter, BsqFormatter bsqFormatter) { + super(dataModel, fiatVolumeValidator, fiatPriceValidator, altcoinValidator, btcValidator, bsqValidator, securityDepositValidator, p2PService, walletsSetup, priceFeedService, navigation, preferences, btcFormatter, bsqFormatter); } } diff --git a/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java index e21a7c0e705..844c3394c41 100644 --- a/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java +++ b/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java @@ -64,6 +64,8 @@ import javax.inject.Inject; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.control.Button; @@ -106,10 +108,6 @@ import static bisq.desktop.util.FormBuilder.addHBoxLabelComboBox; import static bisq.desktop.util.FormBuilder.addTitledGroupBg; - - -import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; - @FxmlView public class OfferBookView extends ActivatableViewAndModel { diff --git a/src/main/java/bisq/desktop/main/portfolio/PortfolioView.fxml b/src/main/java/bisq/desktop/main/portfolio/PortfolioView.fxml index eda330d84c0..12b844b3ffd 100644 --- a/src/main/java/bisq/desktop/main/portfolio/PortfolioView.fxml +++ b/src/main/java/bisq/desktop/main/portfolio/PortfolioView.fxml @@ -22,11 +22,11 @@ - - - + + + diff --git a/src/main/java/bisq/desktop/main/portfolio/PortfolioView.java b/src/main/java/bisq/desktop/main/portfolio/PortfolioView.java index c506e90d8f0..48e921f4e0e 100644 --- a/src/main/java/bisq/desktop/main/portfolio/PortfolioView.java +++ b/src/main/java/bisq/desktop/main/portfolio/PortfolioView.java @@ -26,11 +26,13 @@ import bisq.desktop.common.view.ViewLoader; import bisq.desktop.main.MainView; import bisq.desktop.main.portfolio.closedtrades.ClosedTradesView; +import bisq.desktop.main.portfolio.editoffer.EditOpenOfferView; import bisq.desktop.main.portfolio.failedtrades.FailedTradesView; import bisq.desktop.main.portfolio.openoffer.OpenOffersView; import bisq.desktop.main.portfolio.pendingtrades.PendingTradesView; import bisq.core.locale.Res; +import bisq.core.offer.OpenOffer; import bisq.core.trade.Trade; import bisq.core.trade.failed.FailedTradesManager; @@ -45,19 +47,27 @@ import javafx.collections.ListChangeListener; +import java.util.List; + @FxmlView public class PortfolioView extends ActivatableViewAndModel { @FXML Tab openOffersTab, pendingTradesTab, closedTradesTab; + private Tab editOpenOfferTab; private final Tab failedTradesTab = new Tab(Res.get("portfolio.tab.failed")); private Tab currentTab; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; + private ListChangeListener tabListChangeListener; private final ViewLoader viewLoader; private final Navigation navigation; private final FailedTradesManager failedTradesManager; + private EditOpenOfferView editOpenOfferView; + private boolean editOpenOfferViewOpen; + private OpenOffer openOffer; + private OpenOffersView openOffersView; @Inject public PortfolioView(CachingViewLoader viewLoader, Navigation navigation, FailedTradesManager failedTradesManager) { @@ -90,9 +100,37 @@ else if (newValue == closedTradesTab) else if (newValue == failedTradesTab) //noinspection unchecked navigation.navigateTo(MainView.class, PortfolioView.class, FailedTradesView.class); + else if (newValue == editOpenOfferTab) { + //noinspection unchecked + navigation.navigateTo(MainView.class, PortfolioView.class, EditOpenOfferView.class); + } + + if (oldValue != null && oldValue == editOpenOfferTab && editOpenOfferView != null) + editOpenOfferView.onTabSelected(false); + + }; + + tabListChangeListener = change -> { + change.next(); + List removedTabs = change.getRemoved(); + if (removedTabs.size() == 1) { + if (removedTabs.get(0).equals(editOpenOfferTab)) + onEditOpenOfferRemoved(); + } }; } + private void onEditOpenOfferRemoved() { + editOpenOfferViewOpen = false; + if (editOpenOfferView != null) { + editOpenOfferView.onClose(); + editOpenOfferView = null; + } + + //noinspection unchecked + navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class); + } + @Override protected void activate() { failedTradesManager.getFailedTrades().addListener((ListChangeListener) c -> { @@ -103,6 +141,7 @@ protected void activate() { root.getTabs().add(failedTradesTab); root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); + root.getTabs().addListener(tabListChangeListener); navigation.addListener(navigationListener); if (root.getSelectionModel().getSelectedItem() == openOffersTab) @@ -117,29 +156,85 @@ else if (root.getSelectionModel().getSelectedItem() == closedTradesTab) else if (root.getSelectionModel().getSelectedItem() == failedTradesTab) //noinspection unchecked navigation.navigateTo(MainView.class, PortfolioView.class, FailedTradesView.class); + else if (root.getSelectionModel().getSelectedItem() == editOpenOfferTab) { + //noinspection unchecked + navigation.navigateTo(MainView.class, PortfolioView.class, EditOpenOfferView.class); + if (editOpenOfferView != null) editOpenOfferView.onTabSelected(true); + } } @Override protected void deactivate() { root.getSelectionModel().selectedItemProperty().removeListener(tabChangeListener); + root.getTabs().removeListener(tabListChangeListener); navigation.removeListener(navigationListener); currentTab = null; } private void loadView(Class viewClass) { // we want to get activate/deactivate called, so we remove the old view on tab change - if (currentTab != null) + if (currentTab != null && currentTab != editOpenOfferTab) currentTab.setContent(null); View view = viewLoader.load(viewClass); - if (view instanceof OpenOffersView) currentTab = openOffersTab; - else if (view instanceof PendingTradesView) currentTab = pendingTradesTab; - else if (view instanceof ClosedTradesView) currentTab = closedTradesTab; - else if (view instanceof FailedTradesView) currentTab = failedTradesTab; + if (view instanceof OpenOffersView) { + selectOpenOffersView((OpenOffersView) view); + } else if (view instanceof PendingTradesView) { + currentTab = pendingTradesTab; + } else if (view instanceof ClosedTradesView) { + currentTab = closedTradesTab; + } else if (view instanceof FailedTradesView) { + currentTab = failedTradesTab; + } else if (view instanceof EditOpenOfferView) { + if (openOffer != null) { + if (editOpenOfferView == null) { + + editOpenOfferView = (EditOpenOfferView) view; + editOpenOfferView.initWithData(openOffer); + editOpenOfferTab = new Tab(Res.get("portfolio.tab.editOpenOffer")); + editOpenOfferView.setCloseHandler(() -> { + root.getTabs().remove(editOpenOfferTab); + }); + root.getTabs().add(editOpenOfferTab); + } + if (currentTab != editOpenOfferTab) + editOpenOfferView.onTabSelected(true); + + currentTab = editOpenOfferTab; + } else { + view = viewLoader.load(OpenOffersView.class); + selectOpenOffersView((OpenOffersView) view); + } + + } currentTab.setContent(view.getRoot()); root.getSelectionModel().select(currentTab); } + + private void selectOpenOffersView(OpenOffersView view) { + openOffersView = view; + currentTab = openOffersTab; + + OpenOfferActionHandler openOfferActionHandler = new OpenOfferActionHandler() { + @Override + public void onEditOpenOffer(OpenOffer openOffer) { + if (!editOpenOfferViewOpen) { + PortfolioView.this.editOpenOfferViewOpen = true; + PortfolioView.this.openOffer = openOffer; + PortfolioView.this.navigation.navigateTo(MainView.class, PortfolioView.this.getClass(), + EditOpenOfferView.class); + } else { + log.error("You have already a \"Edit Offer\" tab open."); + } + } + }; + openOffersView.setOpenOfferActionHandler(openOfferActionHandler); + } + + public interface OpenOfferActionHandler { + void onEditOpenOffer(OpenOffer openOffer); + } } diff --git a/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOpenOfferDataModel.java b/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOpenOfferDataModel.java new file mode 100644 index 00000000000..33fbffb1452 --- /dev/null +++ b/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOpenOfferDataModel.java @@ -0,0 +1,132 @@ +/* + * 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.portfolio.editoffer; + + +import bisq.desktop.main.offer.EditableOfferDataModel; +import bisq.desktop.util.BSFormatter; + +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.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.payment.AccountAgeWitnessService; +import bisq.core.provider.fee.FeeService; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.user.Preferences; +import bisq.core.user.User; + +import bisq.network.p2p.P2PService; + +import bisq.common.crypto.KeyRing; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import com.google.inject.Inject; + +class EditOpenOfferDataModel extends EditableOfferDataModel { + + private OpenOffer openOffer; + private OpenOffer.State initialState; + + @Inject + EditOpenOfferDataModel(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(openOfferManager, btcWalletService, bsqWalletService, preferences, user, keyRing, p2PService, priceFeedService, filterManager, accountAgeWitnessService, tradeWalletService, feeService, formatter); + } + + public void initWithData(OpenOffer openOffer) { + this.openOffer = openOffer; + this.initialState = openOffer.getState(); + this.paymentAccount = user.getPaymentAccount(openOffer.getOffer().getMakerPaymentAccountId()); + } + + public void populateData() { + Offer offer = openOffer.getOffer(); + + setAmount(offer.getAmount()); + setMinAmount(offer.getMinAmount()); + setPrice(offer.getPrice()); + setVolume(offer.getVolume()); + setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); + if (offer.isUseMarketBasedPrice()) setMarketPriceMargin(offer.getMarketPriceMargin()); + } + + public void onStartEditOffer(ErrorMessageHandler errorMessageHandler) { + openOfferManager.editOpenOfferStart(openOffer, () -> {}, errorMessageHandler); + } + + public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + final OfferPayload offerPayload = openOffer.getOffer().getOfferPayload(); + final OfferPayload editedPayload = new OfferPayload(offerPayload.getId(), + offerPayload.getDate(), + offerPayload.getOwnerNodeAddress(), + offerPayload.getPubKeyRing(), + offerPayload.getDirection(), + getPrice().get().getValue(), + getMarketPriceMargin(), + isUseMarketBasedPriceValue(), + getAmount().get().getValue(), + getMinAmount().get().getValue(), + offerPayload.getBaseCurrencyCode(), + offerPayload.getCounterCurrencyCode(), + offerPayload.getArbitratorNodeAddresses(), + offerPayload.getMediatorNodeAddresses(), + offerPayload.getPaymentMethodId(), + offerPayload.getMakerPaymentAccountId(), + offerPayload.getOfferFeePaymentTxId(), + offerPayload.getCountryCode(), + offerPayload.getAcceptedCountryCodes(), + offerPayload.getBankId(), + offerPayload.getAcceptedBankIds(), + offerPayload.getVersionNr(), + offerPayload.getBlockHeightAtOfferCreation(), + offerPayload.getTxFee(), + offerPayload.getMakerFee(), + offerPayload.isCurrencyForMakerFeeBtc(), + offerPayload.getBuyerSecurityDeposit(), + offerPayload.getSellerSecurityDeposit(), + offerPayload.getMaxTradeLimit(), + offerPayload.getMaxTradePeriod(), + offerPayload.isUseAutoClose(), + offerPayload.isUseReOpenAfterAutoClose(), + offerPayload.getLowerClosePrice(), + offerPayload.getUpperClosePrice(), + offerPayload.isPrivateOffer(), + offerPayload.getHashOfChallenge(), + offerPayload.getExtraDataMap(), + offerPayload.getProtocolVersion()); + + final Offer editedOffer = new Offer(editedPayload); + editedOffer.setPriceFeedService(priceFeedService); + editedOffer.setState(Offer.State.AVAILABLE); + + openOfferManager.editOpenOfferPublish(editedOffer, initialState, () -> { + openOffer = null; + resultHandler.handleResult(); + }, errorMessageHandler); + } + + public void onCancelEditOffer(ErrorMessageHandler errorMessageHandler) { + if (openOffer != null) + openOfferManager.editOpenOfferCancel(openOffer, initialState, () -> {}, errorMessageHandler); + } +} diff --git a/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOpenOfferView.fxml b/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOpenOfferView.fxml new file mode 100644 index 00000000000..09d2714c84d --- /dev/null +++ b/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOpenOfferView.fxml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOpenOfferView.java b/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOpenOfferView.java new file mode 100644 index 00000000000..8adc76fdbd0 --- /dev/null +++ b/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOpenOfferView.java @@ -0,0 +1,211 @@ +/* + * 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.portfolio.editoffer; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.BusyAnimation; +import bisq.desktop.main.MainView; +import bisq.desktop.main.offer.EditableOfferView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.OfferDetailsWindow; +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.Transitions; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOffer; +import bisq.core.user.DontShowAgainLookup; +import bisq.core.user.Preferences; + +import bisq.common.util.Tuple3; + +import com.google.inject.Inject; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.image.ImageView; + +import javafx.geometry.Insets; + +import static bisq.desktop.util.FormBuilder.addButton; +import static bisq.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; + +@FxmlView +public class EditOpenOfferView extends EditableOfferView { + + private BusyAnimation busyAnimation; + private Button confirmButton; + private Button cancelButton; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private EditOpenOfferView(EditOpenOfferViewModel model, Navigation navigation, Preferences preferences, Transitions transitions, OfferDetailsWindow offerDetailsWindow, BSFormatter btcFormatter, BsqFormatter bsqFormatter) { + super(model, navigation, preferences, transitions, offerDetailsWindow, btcFormatter, bsqFormatter); + } + + @Override + protected void initialize() { + super.initialize(); + + addConfirmEditGroup(); + } + + @Override + protected void doActivate() { + super.doActivate(); + + addBindings(); + + hidePaymentGroup(); + hideOptionsGroup(); + + //workaround to fix margin on top of amount group + gridPane.setPadding(new Insets(-20, 25, -1, 25)); + + updateMarketPriceAvailable(); + updateElementsWithDirection(); + + model.onStartEditOffer(errorMessage -> { + log.error(errorMessage); + new Popup<>().warning(Res.get("editOffer.failed", errorMessage)) + .onClose(() -> { + close(); + }) + .show(); + }); + + model.isNextButtonDisabled.setValue(false); + cancelButton.setDisable(false); + + model.onInvalidateMarketPriceMargin(); + model.onInvalidatePrice(); + } + + @Override + public void onClose() { + model.onCancelEditOffer(errorMessage -> { + log.error(errorMessage); + new Popup<>().warning(Res.get("editOffer.failed", errorMessage)).show(); + }); + } + + @Override + protected void deactivate() { + super.deactivate(); + + removeBindings(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void initWithData(OpenOffer openOffer) { + super.initWithData(openOffer.getOffer().getDirection(), CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()).get()); + model.initWithData(openOffer); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Bindings, Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addBindings() { + confirmButton.disableProperty().bind(model.isNextButtonDisabled); + } + + private void removeBindings() { + confirmButton.disableProperty().unbind(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Build UI elements + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addConfirmEditGroup() { + + int tmpGridRow = 4; + final Tuple3 editOfferTuple = addButtonBusyAnimationLabelAfterGroup(gridPane, tmpGridRow++, Res.get("editOffer.confirmEdit")); + + confirmButton = editOfferTuple.first; + confirmButton.setMinHeight(40); + confirmButton.setPadding(new Insets(0, 20, 0, 20)); + confirmButton.setGraphicTextGap(10); + + + busyAnimation = editOfferTuple.second; + Label spinnerInfoLabel = editOfferTuple.third; + + + cancelButton = addButton(gridPane, tmpGridRow, Res.get("shared.cancel")); + cancelButton.setDefaultButton(false); + cancelButton.setId("cancel-button"); + cancelButton.setOnAction(event -> close()); + + confirmButton.setOnAction(e -> { + + if (model.isPriceInRange()) { + + model.isNextButtonDisabled.setValue(true); + cancelButton.setDisable(true); + busyAnimation.play(); + spinnerInfoLabel.setText(Res.get("editOffer.publishOffer")); + //edit offer + model.onPublishOffer(() -> { + log.debug("Edit offer was successful"); + + String key = "ShowOpenOffersAfterEditing"; + + if (DontShowAgainLookup.showAgain(key)) + //noinspection unchecked + new Popup<>().feedback(Res.get("editOffer.success")) + .actionButtonTextWithGoTo("navigation.portfolio.myOpenOffers") + .onAction(() -> navigation.navigateTo(MainView.class, PortfolioView.class, OpenOffersView.class)) + .dontShowAgainId(key) + .show(); + spinnerInfoLabel.setText(""); + close(); + }, (message) -> { + log.error(message); + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + model.isNextButtonDisabled.setValue(false); + cancelButton.setDisable(false); + new Popup<>().warning(Res.get("editOffer.failed", message)).show(); + }); + } + }); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateElementsWithDirection() { + ImageView iconView = new ImageView(); + iconView.setId(model.isSellOffer() ? "image-sell-white" : "image-buy-white"); + confirmButton.setGraphic(iconView); + confirmButton.setId(model.isSellOffer() ? "sell-button-big" : "buy-button-big"); + } +} diff --git a/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOpenOfferViewModel.java b/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOpenOfferViewModel.java new file mode 100644 index 00000000000..261cd9e66b2 --- /dev/null +++ b/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOpenOfferViewModel.java @@ -0,0 +1,81 @@ +/* + * 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.portfolio.editoffer; + +import bisq.desktop.Navigation; +import bisq.desktop.main.offer.EditableOfferViewModel; +import bisq.desktop.util.BSFormatter; +import bisq.desktop.util.BsqFormatter; +import bisq.desktop.util.validation.AltcoinValidator; +import bisq.desktop.util.validation.BsqValidator; +import bisq.desktop.util.validation.BtcValidator; +import bisq.desktop.util.validation.FiatPriceValidator; +import bisq.desktop.util.validation.FiatVolumeValidator; +import bisq.desktop.util.validation.SecurityDepositValidator; + +import bisq.core.btc.wallet.WalletsSetup; +import bisq.core.offer.OpenOffer; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.user.Preferences; + +import bisq.network.p2p.P2PService; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import com.google.inject.Inject; + +class EditOpenOfferViewModel extends EditableOfferViewModel { + + @Inject + public EditOpenOfferViewModel(EditOpenOfferDataModel dataModel, FiatVolumeValidator fiatVolumeValidator, FiatPriceValidator fiatPriceValidator, AltcoinValidator altcoinValidator, BtcValidator btcValidator, BsqValidator bsqValidator, SecurityDepositValidator securityDepositValidator, P2PService p2PService, WalletsSetup walletsSetup, PriceFeedService priceFeedService, Navigation navigation, Preferences preferences, BSFormatter btcFormatter, BsqFormatter bsqFormatter) { + super(dataModel, fiatVolumeValidator, fiatPriceValidator, altcoinValidator, btcValidator, bsqValidator, securityDepositValidator, p2PService, walletsSetup, priceFeedService, navigation, preferences, btcFormatter, bsqFormatter); + } + + @Override + public void activate() { + super.activate(); + dataModel.populateData(); + } + + public void initWithData(OpenOffer openOffer) { + dataModel.initWithData(openOffer); + } + + public void onStartEditOffer(ErrorMessageHandler errorMessageHandler) { + dataModel.onStartEditOffer(errorMessageHandler); + } + + public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + dataModel.onPublishOffer(resultHandler, errorMessageHandler); + } + + public void onCancelEditOffer(ErrorMessageHandler errorMessageHandler) { + dataModel.onCancelEditOffer(errorMessageHandler); + } + + public void onInvalidateMarketPriceMargin() { + marketPriceMargin.set("0.00%"); + marketPriceMargin.set(btcFormatter.formatToPercent(dataModel.getMarketPriceMargin())); + } + + public void onInvalidatePrice() { + price.set(btcFormatter.formatPrice(null)); + price.set(btcFormatter.formatPrice(dataModel.getPrice().get())); + } +} diff --git a/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml b/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml index 2768248d8cc..409286df493 100644 --- a/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml +++ b/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml @@ -36,8 +36,9 @@ - - + + + diff --git a/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index eb148f186c2..5eadd143f1e 100644 --- a/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -20,7 +20,7 @@ import bisq.desktop.Navigation; import bisq.desktop.common.view.ActivatableViewAndModel; import bisq.desktop.common.view.FxmlView; -import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipCheckBox; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.main.MainView; @@ -28,6 +28,7 @@ import bisq.desktop.main.funds.withdrawal.WithdrawalView; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.OfferDetailsWindow; +import bisq.desktop.main.portfolio.PortfolioView; import bisq.core.locale.Res; import bisq.core.monetary.Price; @@ -37,9 +38,12 @@ import javax.inject.Inject; +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + import javafx.fxml.FXML; import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; import javafx.scene.control.TableView; @@ -55,6 +59,8 @@ import org.jetbrains.annotations.NotNull; +import static bisq.desktop.util.FormBuilder.getIconButton; + @FxmlView public class OpenOffersView extends ActivatableViewAndModel { @@ -62,10 +68,12 @@ public class OpenOffersView extends ActivatableViewAndModel tableView; @FXML TableColumn priceColumn, amountColumn, volumeColumn, - marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, removeItemColumn; + marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, + removeItemColumn, editItemColumn; private final Navigation navigation; private final OfferDetailsWindow offerDetailsWindow; private SortedList sortedList; + private PortfolioView.OpenOfferActionHandler openOfferActionHandler; @Inject public OpenOffersView(OpenOffersViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow) { @@ -83,7 +91,8 @@ public void initialize() { directionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerType"))); dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime"))); offerIdColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerId"))); - deactivateItemColumn.setGraphic(new AutoTooltipLabel("")); + deactivateItemColumn.setGraphic(new AutoTooltipLabel("Enabled")); + editItemColumn.setGraphic(new AutoTooltipLabel("")); removeItemColumn.setGraphic(new AutoTooltipLabel("")); setOfferIdColumnCellFactory(); @@ -94,6 +103,7 @@ public void initialize() { setVolumeColumnCellFactory(); setDateColumnCellFactory(); setDeactivateColumnCellFactory(); + setEditColumnCellFactory(); setRemoveColumnCellFactory(); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); @@ -148,7 +158,7 @@ private void onDeactivateOpenOffer(OpenOffer openOffer) { private void onActivateOpenOffer(OpenOffer openOffer) { if (model.isBootstrapped()) { - model.onActivateOpenOffer(openOffer, + model.onActivateOpenOffer(openOffer, () -> { log.debug("Activate offer was successful"); }, @@ -200,6 +210,14 @@ private void doRemoveOpenOffer(OpenOffer openOffer) { }); } + private void onEditOpenOffer(OpenOffer openOffer) { + if (model.isBootstrapped()) { + openOfferActionHandler.onEditOpenOffer(openOffer); + } else { + new Popup<>().information(Res.get("popup.warning.notFullyConnected")).show(); + } + } + private void setOfferIdColumnCellFactory() { offerIdColumn.setCellValueFactory((openOfferListItem) -> new ReadOnlyObjectWrapper<>(openOfferListItem.getValue())); offerIdColumn.setCellFactory( @@ -270,7 +288,7 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); getStyleClass().removeAll("offer-disabled"); - if(item != null) { + if (item != null) { if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); setGraphic(new AutoTooltipLabel(model.getAmount(item))); } else { @@ -296,7 +314,7 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); getStyleClass().removeAll("offer-disabled"); - if(item != null) { + if (item != null) { if (model.isDeactivated(item)) getStyleClass().add("offer-disabled"); setGraphic(new AutoTooltipLabel(model.getPrice(item))); } else { @@ -394,17 +412,13 @@ private void setDeactivateColumnCellFactory() { public TableCell call(TableColumn column) { return new TableCell() { final ImageView iconView = new ImageView(); - Button button; + CheckBox checkBox; private void updateState(@NotNull OpenOffer openOffer) { if (openOffer.isDeactivated()) { - button.setText(Res.get("shared.activate")); - iconView.setId("image-alert-round"); - button.setGraphic(iconView); + checkBox.setSelected(false); } else { - button.setText(Res.get("shared.deactivate")); - iconView.setId("image-green_circle"); - button.setGraphic(iconView); + checkBox.setSelected(true); } } @@ -413,14 +427,13 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { - if (button == null) { - button = new AutoTooltipButton(); - button.setGraphic(iconView); + if (checkBox == null) { + checkBox = new AutoTooltipCheckBox(); + checkBox.setGraphic(iconView); updateState(item.getOpenOffer()); - button.setMinWidth(70); - setGraphic(button); + setGraphic(checkBox); } - button.setOnAction(event -> { + checkBox.setOnAction(event -> { if (item.getOpenOffer().isDeactivated()) { onActivateOpenOffer(item.getOpenOffer()); } else { @@ -431,9 +444,9 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }); } else { setGraphic(null); - if (button != null) { - button.setOnAction(null); - button = null; + if (checkBox != null) { + checkBox.setOnAction(null); + checkBox = null; } } } @@ -449,7 +462,6 @@ private void setRemoveColumnCellFactory() { @Override public TableCell call(TableColumn column) { return new TableCell() { - final ImageView iconView = new ImageView(); Button button; @Override @@ -458,10 +470,7 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { if (item != null && !empty) { if (button == null) { - button = new AutoTooltipButton(Res.get("shared.remove")); - button.setMinWidth(70); - iconView.setId("image-remove"); - button.setGraphic(iconView); + button = getIconButton(MaterialDesignIcon.DELETE_FOREVER, "delete"); setGraphic(button); } button.setOnAction(event -> onRemoveOpenOffer(item.getOpenOffer())); @@ -477,5 +486,41 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { } }); } + + private void setEditColumnCellFactory() { + editItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + editItemColumn.setCellFactory( + new Callback, TableCell>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + Button button; + + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + if (button == null) { + button = getIconButton(MaterialDesignIcon.PENCIL); + setGraphic(button); + } + button.setOnAction(event -> onEditOpenOffer(item.getOpenOffer())); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + } + + public void setOpenOfferActionHandler(PortfolioView.OpenOfferActionHandler openOfferActionHandler) { + this.openOfferActionHandler = openOfferActionHandler; + } } diff --git a/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java b/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java index 60ede269713..3a2f2b36d9c 100644 --- a/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java +++ b/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersViewModel.java @@ -51,7 +51,11 @@ public OpenOffersViewModel(OpenOffersDataModel dataModel, } void onActivateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - dataModel.onActivateOpenOffer(openOffer, resultHandler, errorMessageHandler); + try { + dataModel.onActivateOpenOffer(openOffer, resultHandler, errorMessageHandler); + } catch (IllegalStateException exception) { + errorMessageHandler.handleErrorMessage(exception.getMessage()); + } } void onDeactivateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -59,7 +63,11 @@ void onDeactivateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, Err } void onRemoveOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { - dataModel.onRemoveOpenOffer(openOffer, resultHandler, errorMessageHandler); + try { + dataModel.onRemoveOpenOffer(openOffer, resultHandler, errorMessageHandler); + } catch (IllegalStateException exception) { + errorMessageHandler.handleErrorMessage(exception.getMessage()); + } } public ObservableList getList() { diff --git a/src/main/java/bisq/desktop/util/FormBuilder.java b/src/main/java/bisq/desktop/util/FormBuilder.java index ff42beffa39..70ecc01291f 100644 --- a/src/main/java/bisq/desktop/util/FormBuilder.java +++ b/src/main/java/bisq/desktop/util/FormBuilder.java @@ -44,6 +44,8 @@ import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeIcon; +import de.jensd.fx.glyphs.GlyphIcons; +import de.jensd.fx.glyphs.materialdesignicons.utils.MaterialDesignIconFactory; import javafx.scene.Node; import javafx.scene.control.Button; @@ -77,11 +79,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; - - -import de.jensd.fx.glyphs.GlyphIcons; -import de.jensd.fx.glyphs.materialdesignicons.utils.MaterialDesignIconFactory; - public class FormBuilder { private static final Logger log = LoggerFactory.getLogger(FormBuilder.class); public static final String MATERIAL_DESIGN_ICONS = "'Material Design Icons'"; @@ -1247,10 +1244,14 @@ public static Label getIcon(AwesomeIcon icon) { } public static Button getIconButton(GlyphIcons icon) { + return getIconButton(icon, "highlight"); + } + + public static Button getIconButton(GlyphIcons icon, String styleClass) { if (icon.fontFamily().equals(MATERIAL_DESIGN_ICONS)) { final Button textIcon = MaterialDesignIconFactory.get().createIconButton(icon, "","2em", null, ContentDisplay.CENTER); textIcon.setId("icon-button"); - textIcon.getGraphic().getStyleClass().add("highlight"); + textIcon.getGraphic().getStyleClass().add(styleClass); textIcon.setPrefWidth(20); textIcon.setPrefHeight(20); textIcon.setPadding(new Insets(0));