/* * This file is part of Bitsquare. * * Bitsquare 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. * * Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>. */ package io.bitsquare.gui.main.offer.createoffer; import io.bitsquare.app.DevFlags; import io.bitsquare.btc.pricefeed.MarketPrice; import io.bitsquare.btc.pricefeed.PriceFeedService; import io.bitsquare.common.Timer; import io.bitsquare.common.UserThread; import io.bitsquare.common.util.MathUtils; import io.bitsquare.gui.Navigation; import io.bitsquare.gui.common.model.ActivatableWithDataModel; import io.bitsquare.gui.common.model.ViewModel; import io.bitsquare.gui.main.MainView; import io.bitsquare.gui.main.funds.FundsView; import io.bitsquare.gui.main.funds.deposit.DepositView; import io.bitsquare.gui.main.offer.createoffer.monetary.Altcoin; import io.bitsquare.gui.main.offer.createoffer.monetary.Price; import io.bitsquare.gui.main.offer.createoffer.monetary.Volume; import io.bitsquare.gui.main.overlays.popups.Popup; import io.bitsquare.gui.main.settings.SettingsView; import io.bitsquare.gui.main.settings.preferences.PreferencesView; import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.validation.BtcValidator; import io.bitsquare.gui.util.validation.FiatValidator; import io.bitsquare.gui.util.validation.InputValidator; import io.bitsquare.locale.BSResources; import io.bitsquare.locale.CurrencyUtil; import io.bitsquare.locale.TradeCurrency; import io.bitsquare.p2p.P2PService; import io.bitsquare.payment.PaymentAccount; import io.bitsquare.trade.offer.Offer; import io.bitsquare.user.Preferences; import javafx.beans.property.*; import javafx.beans.value.ChangeListener; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import javax.inject.Inject; import java.util.concurrent.TimeUnit; import static javafx.beans.binding.Bindings.createStringBinding; class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel> implements ViewModel { private final BtcValidator btcValidator; private final P2PService p2PService; private PriceFeedService priceFeedService; private Preferences preferences; private Navigation navigation; final BSFormatter formatter; private final FiatValidator fiatValidator; 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(); // 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(); // Positive % value means always a better price form the offerer'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 btcCode = 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(); final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); final BooleanProperty isWaitingForFunds = new SimpleBooleanProperty(); final ObjectProperty<InputValidator.ValidationResult> amountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty<InputValidator.ValidationResult> minAmountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty<InputValidator.ValidationResult> priceValidationResult = new SimpleObjectProperty<>(); final ObjectProperty<InputValidator.ValidationResult> volumeValidationResult = new SimpleObjectProperty<>(); // Those are needed for the addressTextField final ObjectProperty<Address> address = new SimpleObjectProperty<>(); private ChangeListener<String> amountStringListener; private ChangeListener<String> minAmountStringListener; private ChangeListener<String> priceStringListener, marketPriceMarginStringListener; private ChangeListener<String> volumeStringListener; private ChangeListener<Coin> amountAsCoinListener; private ChangeListener<Coin> minAmountAsCoinListener; private ChangeListener<Price> priceListener; private ChangeListener<Volume> volumeListener; private ChangeListener<Boolean> isWalletFundedListener; //private ChangeListener<Coin> feeFromFundingTxListener; private ChangeListener<String> errorMessageListener; private Offer offer; private Timer timeoutTimer; private boolean inputIsMarketBasedPrice; private ChangeListener<Boolean> useMarketBasedPriceListener; private boolean ignorePriceStringListener, ignoreVolumeStringListener, ignoreAmountStringListener; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject public CreateOfferViewModel(CreateOfferDataModel dataModel, FiatValidator fiatValidator, BtcValidator btcValidator, P2PService p2PService, PriceFeedService priceFeedService, Preferences preferences, Navigation navigation, BSFormatter formatter) { super(dataModel); this.fiatValidator = fiatValidator; this.btcValidator = btcValidator; this.p2PService = p2PService; this.priceFeedService = priceFeedService; this.preferences = preferences; this.navigation = navigation; this.formatter = formatter; paymentLabel = BSResources.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId); if (dataModel.getAddressEntry() != null) { addressAsString = dataModel.getAddressEntry().getAddressString(); address.set(dataModel.getAddressEntry().getAddress()); } createListeners(); } @Override protected void activate() { if (DevFlags.DEV_MODE) { UserThread.runAfter(() -> { amount.set("1"); minAmount.set(amount.get()); price.set("1000"); setAmountToModel(); setMinAmountToModel(); setPriceToModel(); dataModel.calculateVolume(); dataModel.calculateTotalToPay(); updateButtonDisableState(); updateSpinnerInfo(); }, 10, TimeUnit.MILLISECONDS); } addBindings(); addListeners(); updateButtonDisableState(); if (dataModel.getDirection() == Offer.Direction.BUY) { directionLabel = BSResources.get("shared.buyBitcoin"); amountDescription = BSResources.get("createOffer.amountPriceBox.amountDescription", BSResources.get("shared.buy")); } else { directionLabel = BSResources.get("shared.sellBitcoin"); amountDescription = BSResources.get("createOffer.amountPriceBox.amountDescription", BSResources.get("shared.sell")); } } @Override protected void deactivate() { removeBindings(); removeListeners(); stopTimeoutTimer(); } private void addBindings() { if (dataModel.getDirection() == Offer.Direction.BUY) { volumeDescriptionLabel.bind(createStringBinding( () -> BSResources.get("createOffer.amountPriceBox.buy.volumeDescription", dataModel.tradeCurrencyCode.get()), dataModel.tradeCurrencyCode)); } else { volumeDescriptionLabel.bind(createStringBinding( () -> BSResources.get("createOffer.amountPriceBox.sell.volumeDescription", dataModel.tradeCurrencyCode.get()), dataModel.tradeCurrencyCode)); } volumePromptLabel.bind(createStringBinding( () -> BSResources.get("createOffer.volume.prompt", dataModel.tradeCurrencyCode.get()), dataModel.tradeCurrencyCode)); totalToPay.bind(createStringBinding(() -> formatter.formatCoinWithCode(dataModel.totalToPayAsCoin.get()), dataModel.totalToPayAsCoin)); tradeAmount.bind(createStringBinding(() -> formatter.formatCoinWithCode(dataModel.amount.get()), dataModel.amount)); btcCode.bind(dataModel.btcCode); tradeCurrencyCode.bind(dataModel.tradeCurrencyCode); } private void removeBindings() { totalToPay.unbind(); tradeAmount.unbind(); btcCode.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) -> { if (!ignorePriceStringListener) { if (isPriceInputValid(newValue).isValid) { setPriceToModel(); dataModel.calculateVolume(); dataModel.calculateTotalToPay(); if (!inputIsMarketBasedPrice) { final String currencyCode = dataModel.tradeCurrencyCode.get(); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); if (marketPrice != null) { double marketPriceAsDouble = marketPrice.getPrice(getPriceFeedType()); try { double priceAsDouble = formatter.parseNumberStringToDouble(price.get()); double relation = priceAsDouble / marketPriceAsDouble; double percentage; if (CurrencyUtil.isCryptoCurrency(currencyCode)) percentage = dataModel.getDirection() == Offer.Direction.SELL ? 1 - relation : relation - 1; else percentage = dataModel.getDirection() == Offer.Direction.BUY ? 1 - relation : relation - 1; percentage = MathUtils.roundDouble(percentage, 4); marketPriceMargin.set(formatter.formatToPercent(percentage)); } catch (NumberFormatException t) { marketPriceMargin.set(""); new Popup().warning("Input is not a valid number.").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 = formatter.parsePercentStringToDouble(newValue); if (percentage >= 1 || percentage <= -1) { new Popup().warning("You cannot set a percentage of 100% or larger. Please enter a percentage number like \"5.4\" for 5.4%") .show(); } else { final String currencyCode = dataModel.tradeCurrencyCode.get(); MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); if (marketPrice != null) { percentage = MathUtils.roundDouble(percentage, 4); dataModel.setMarketPriceMargin(percentage); double marketPriceAsDouble = marketPrice.getPrice(getPriceFeedType()); double factor; if (CurrencyUtil.isCryptoCurrency(currencyCode)) factor = dataModel.getDirection() == Offer.Direction.SELL ? 1 - percentage : 1 + percentage; else factor = dataModel.getDirection() == Offer.Direction.BUY ? 1 - percentage : 1 + percentage; double targetPrice = marketPriceAsDouble * factor; int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : 2; ignorePriceStringListener = true; price.set(formatter.formatRoundedDoubleWithPrecision(targetPrice, precision)); ignorePriceStringListener = false; setPriceToModel(); dataModel.calculateVolume(); dataModel.calculateTotalToPay(); updateButtonDisableState(); } else { new Popup().warning("There is no price feed available for that currency. You cannot use a percent based price.\n" + "Please select the fixed price.") .show(); marketPriceMargin.set(""); } } } } catch (Throwable t) { new Popup().warning("Your input is not a valid number. Please enter a percentage number like \"5.4\" for 5.4%") .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(); } }; amountAsCoinListener = (ov, oldValue, newValue) -> { if (newValue != null) amount.set(formatter.formatCoin(newValue)); else amount.set(""); }; minAmountAsCoinListener = (ov, oldValue, newValue) -> { if (newValue != null) minAmount.set(formatter.formatCoin(newValue)); else minAmount.set(""); }; priceListener = (ov, oldValue, newValue) -> { ignorePriceStringListener = true; if (newValue != null) price.set(newValue.toString()); else price.set(""); ignorePriceStringListener = false; }; volumeListener = (ov, oldValue, newValue) -> { ignoreVolumeStringListener = true; if (newValue != null) volume.set(newValue.toString()); else volume.set(""); ignoreVolumeStringListener = false; }; isWalletFundedListener = (ov, oldValue, newValue) -> updateButtonDisableState(); /* feeFromFundingTxListener = (ov, oldValue, newValue) -> { updateButtonDisableState(); };*/ } 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.useMarketBasedPrice.addListener(useMarketBasedPriceListener); volume.addListener(volumeStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.amount.addListener(amountAsCoinListener); dataModel.minAmount.addListener(minAmountAsCoinListener); dataModel.price.addListener(priceListener); dataModel.volume.addListener(volumeListener); // dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener); dataModel.isWalletFunded.addListener(isWalletFundedListener); } private void removeListeners() { amount.removeListener(amountStringListener); minAmount.removeListener(minAmountStringListener); price.removeListener(priceStringListener); marketPriceMargin.removeListener(marketPriceMarginStringListener); dataModel.useMarketBasedPrice.removeListener(useMarketBasedPriceListener); volume.removeListener(volumeStringListener); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding dataModel.amount.removeListener(amountAsCoinListener); dataModel.minAmount.removeListener(minAmountAsCoinListener); dataModel.price.removeListener(priceListener); dataModel.volume.removeListener(volumeListener); //dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); dataModel.isWalletFunded.removeListener(isWalletFundedListener); if (offer != null && errorMessageListener != null) offer.errorMessageProperty().removeListener(errorMessageListener); } /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// boolean initWithData(Offer.Direction direction, TradeCurrency tradeCurrency) { boolean result = dataModel.initWithData(direction, tradeCurrency); if (dataModel.paymentAccount != null) btcValidator.setMaxTradeLimitInBitcoin(dataModel.paymentAccount.getPaymentMethod().getMaxTradeLimit()); 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("A timeout occurred at publishing the offer."); updateButtonDisableState(); updateSpinnerInfo(); resultHandler.run(); }, 30); } errorMessageListener = (observable, oldValue, newValue) -> { if (newValue != null) { stopTimeoutTimer(); createOfferRequested = false; if (offer.getState() == Offer.State.OFFER_FEE_PAID) errorMessage.set(newValue + "\n\nThe offer fee is already paid. In the worst case you have lost that fee. " + "We are sorry about that but keep in mind it is a very small amount.\n" + "Please try to restart you application and check your network connection to see if you can resolve the issue."); 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) { btcValidator.setMaxTradeLimitInBitcoin(paymentAccount.getPaymentMethod().getMaxTradeLimit()); dataModel.onPaymentAccountSelected(paymentAccount); if (amount.get() != null) amountValidationResult.set(isBtcInputValid(amount.get())); } public void onCurrencySelected(TradeCurrency tradeCurrency) { dataModel.onCurrencySelected(tradeCurrency); } void onShowPayFundsScreen() { showPayFundsScreenDisplayed.set(true); updateSpinnerInfo(); } boolean fundFromSavingsWallet() { dataModel.fundFromSavingsWallet(); if (dataModel.isWalletFunded.get()) { updateButtonDisableState(); return true; } else { new Popup().warning("You don't have enough funds in your Bitsquare wallet.\n" + "You need " + formatter.formatCoinWithCode(dataModel.totalToPayAsCoin.get()) + " but you have only " + formatter.formatCoinWithCode(dataModel.totalAvailableBalance) + " in your Bitsquare wallet.\n\n" + "Please fund that trade from an external Bitcoin wallet or fund your Bitsquare " + "wallet at \"Funds/Depost funds\".") .actionButtonText("Go to \"Funds/Deposit funds\"") .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) .show(); return false; } } /////////////////////////////////////////////////////////////////////////////////////////// // Handle focus /////////////////////////////////////////////////////////////////////////////////////////// // On focus out we do validation and apply the data to the model void onFocusOutAmountTextField(boolean oldValue, boolean newValue, String userInput) { if (oldValue && !newValue) { InputValidator.ValidationResult result = isBtcInputValid(amount.get()); amountValidationResult.set(result); if (result.isValid) { setAmountToModel(); ignoreAmountStringListener = true; amount.set(formatter.formatCoin(dataModel.amount.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, String userInput) { if (oldValue && !newValue) { InputValidator.ValidationResult result = isBtcInputValid(minAmount.get()); minAmountValidationResult.set(result); if (result.isValid) { setMinAmountToModel(); minAmount.set(formatter.formatCoin(dataModel.minAmount.get())); if (!dataModel.isMinAmountLessOrEqualAmount()) { amount.set(minAmount.get()); } else { minAmountValidationResult.set(result); if (amount.get() != null) amountValidationResult.set(isBtcInputValid(amount.get())); } } } } void onFocusOutPriceTextField(boolean oldValue, boolean newValue, String userInput) { if (oldValue && !newValue) { InputValidator.ValidationResult result = isPriceInputValid(price.get()); boolean isValid = result.isValid; priceValidationResult.set(result); if (isValid) { setPriceToModel(); ignorePriceStringListener = true; if (dataModel.price.get() != null) price.set(dataModel.price.get().toString()); ignorePriceStringListener = false; dataModel.calculateVolume(); dataModel.calculateAmount(); } } } void onFocusOutPriceAsPercentageTextField(boolean oldValue, boolean newValue, String userInput) { inputIsMarketBasedPrice = !oldValue && newValue; if (oldValue && !newValue) marketPriceMargin.set(formatter.formatRoundedDoubleWithPrecision(dataModel.getMarketPriceMargin() * 100, 2)); } void onFocusOutVolumeTextField(boolean oldValue, boolean newValue, String userInput) { if (oldValue && !newValue) { InputValidator.ValidationResult result = isVolumeInputValid(volume.get()); volumeValidationResult.set(result); if (result.isValid) { setVolumeToModel(); ignoreVolumeStringListener = true; if (dataModel.volume.get() != null) volume.set(dataModel.volume.get().toString()); 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())); } } } } /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// public boolean isPriceInRange() { if (marketPriceMargin.get() != null && !marketPriceMargin.get().isEmpty()) { if (Math.abs(formatter.parsePercentStringToDouble(marketPriceMargin.get())) > preferences.getMaxPriceDistanceInPercent()) { displayPriceOutOfRangePopup(); return false; } else { return true; } } else { return true; } } private void displayPriceOutOfRangePopup() { Popup popup = new Popup(); popup.warning("The price you have entered is outside the max. allowed deviation from the market price.\n" + "The max. allowed deviation is " + formatter.formatToPercentWithSymbol(preferences.getMaxPriceDistanceInPercent()) + " and can be adjusted in the preferences.") .actionButtonText("Change price") .onAction(() -> popup.hide()) .closeButtonText("Go to \"Preferences\"") .onClose(() -> navigation.navigateTo(MainView.class, SettingsView.class, PreferencesView.class)) .show(); } BSFormatter getFormatter() { return formatter; } boolean isSellOffer() { return dataModel.getDirection() == Offer.Direction.SELL; } public TradeCurrency getTradeCurrency() { return dataModel.getTradeCurrency(); } public String getOfferFee() { return formatter.formatCoinWithCode(dataModel.getOfferFeeAsCoin()); } public String getNetworkFee() { return formatter.formatCoinWithCode(dataModel.getNetworkFeeAsCoin()); } public String getSecurityDeposit() { return formatter.formatCoinWithCode(dataModel.getSecurityDepositAsCoin()); } 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 formatter.formatCoin(coin); } public Offer createAndGetOffer() { offer = dataModel.createAndGetOffer(); return offer; } boolean hasAcceptedArbitrators() { return dataModel.hasAcceptedArbitrators(); } boolean isBootstrapped() { return p2PService.isBootstrapped(); } /////////////////////////////////////////////////////////////////////////////////////////// // Utils /////////////////////////////////////////////////////////////////////////////////////////// private void setAmountToModel() { if (amount.get() != null && !amount.get().isEmpty()) { dataModel.amount.set(formatter.parseToCoinWith4Decimals(amount.get())); if (dataModel.minAmount.get() == null || dataModel.minAmount.get().equals(Coin.ZERO)) { minAmount.set(amount.get()); setMinAmountToModel(); } } else { dataModel.amount.set(null); } } private void setMinAmountToModel() { if (minAmount.get() != null && !minAmount.get().isEmpty()) dataModel.minAmount.set(formatter.parseToCoinWith4Decimals(minAmount.get())); else dataModel.minAmount.set(null); } private void setPriceToModel() { if (price.get() != null && !price.get().isEmpty()) { try { final Price price = Price.parse(this.price.get(), dataModel.tradeCurrencyCode.get()); dataModel.price.set(price); } catch (Throwable t) { log.debug(t.getMessage()); } } else { dataModel.price.set(null); } } private void setVolumeToModel() { if (volume.get() != null && !volume.get().isEmpty()) { try { final Volume value = Volume.parse(volume.get(), dataModel.tradeCurrencyCode.get()); dataModel.volume.set(value); } catch (Throwable t) { log.debug(t.getMessage()); } } else { dataModel.volume.set(null); } } private InputValidator.ValidationResult isBtcInputValid(String input) { return btcValidator.validate(input); } private InputValidator.ValidationResult isPriceInputValid(String input) { if (CurrencyUtil.isCryptoCurrency(getTradeCurrency().getCode())) fiatValidator.setMinValue(0.00000001); else fiatValidator.setMinValue(FiatValidator.MIN_FIAT_VALUE); return fiatValidator.validate(input); } private InputValidator.ValidationResult isVolumeInputValid(String input) { if (CurrencyUtil.isCryptoCurrency(getTradeCurrency().getCode())) fiatValidator.setMinValue(0.01); else fiatValidator.setMinValue(FiatValidator.MIN_FIAT_VALUE); return fiatValidator.validate(input); } private void updateSpinnerInfo() { if (!showPayFundsScreenDisplayed.get() || errorMessage.get() != null || showTransactionPublishedScreen.get()) { waitingForFundsText.set(""); } else if (dataModel.isWalletFunded.get()) { waitingForFundsText.set(""); /* if (dataModel.isFeeFromFundingTxSufficient.get()) { spinnerInfoText.set(""); } else { spinnerInfoText.set("Check if funding tx miner fee is sufficient..."); }*/ } else { waitingForFundsText.set("Waiting for funds..."); } 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 && dataModel.price.get() != null && dataModel.price.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.isWalletFunded.get()); } private PriceFeedService.Type getPriceFeedType() { if (CurrencyUtil.isCryptoCurrency(tradeCurrencyCode.get())) return dataModel.getDirection() == Offer.Direction.BUY ? PriceFeedService.Type.ASK : PriceFeedService.Type.BID; else return dataModel.getDirection() == Offer.Direction.SELL ? PriceFeedService.Type.ASK : PriceFeedService.Type.BID; } private void stopTimeoutTimer() { if (timeoutTimer != null) { timeoutTimer.stop(); timeoutTimer = null; } } }