From 04845e43827583797135f494aedd54a4bcd20a03 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Thu, 31 Mar 2016 19:29:54 +0200 Subject: [PATCH] Savings wallet (WIP) --- .../main/java/io/bitsquare/trade/Trade.java | 2 + .../java/io/bitsquare/trade/TradeManager.java | 13 +- .../trade/offer/OpenOfferManager.java | 5 +- .../trade/protocol/trade/ProcessModel.java | 8 +- .../tasks/taker/CreateTakeOfferFeeTx.java | 2 +- .../gui/components/BalanceTextField.java | 15 ++- .../io/bitsquare/gui/main/MainViewModel.java | 8 ++ .../createoffer/CreateOfferDataModel.java | 9 +- .../offer/createoffer/CreateOfferView.java | 13 +- .../createoffer/CreateOfferViewModel.java | 4 +- .../offer/takeoffer/TakeOfferDataModel.java | 103 +++++++++++---- .../main/offer/takeoffer/TakeOfferView.java | 123 ++++++++++++------ .../offer/takeoffer/TakeOfferViewModel.java | 102 +++++++++------ 13 files changed, 280 insertions(+), 127 deletions(-) diff --git a/core/src/main/java/io/bitsquare/trade/Trade.java b/core/src/main/java/io/bitsquare/trade/Trade.java index 0efe6a1d6c..1b80d0c4d9 100644 --- a/core/src/main/java/io/bitsquare/trade/Trade.java +++ b/core/src/main/java/io/bitsquare/trade/Trade.java @@ -221,6 +221,7 @@ public abstract class Trade implements Tradable, Model { OpenOfferManager openOfferManager, User user, KeyRing keyRing, + boolean useSavingsWallet, Coin fundsNeededForTrade) { Log.traceCall(); processModel.onAllServicesInitialized(offer, @@ -232,6 +233,7 @@ public abstract class Trade implements Tradable, Model { arbitratorManager, user, keyRing, + useSavingsWallet, fundsNeededForTrade); createProtocol(); diff --git a/core/src/main/java/io/bitsquare/trade/TradeManager.java b/core/src/main/java/io/bitsquare/trade/TradeManager.java index f285f940be..720cd0ce9d 100644 --- a/core/src/main/java/io/bitsquare/trade/TradeManager.java +++ b/core/src/main/java/io/bitsquare/trade/TradeManager.java @@ -178,7 +178,7 @@ public class TradeManager { else {*/ trade.setStorage(tradableListStorage); trade.updateDepositTxFromWallet(tradeWalletService); - initTrade(trade, trade.getProcessModel().getFundsNeededForTrade()); + initTrade(trade, trade.getProcessModel().getUseSavingsWallet(), trade.getProcessModel().getFundsNeededForTrade()); // } @@ -209,7 +209,7 @@ public class TradeManager { trade = new SellerAsOffererTrade(offer, tradableListStorage); trade.setStorage(tradableListStorage); - initTrade(trade, trade.getProcessModel().getFundsNeededForTrade()); + initTrade(trade, trade.getProcessModel().getUseSavingsWallet(), trade.getProcessModel().getFundsNeededForTrade()); trades.add(trade); ((OffererTrade) trade).handleTakeOfferRequest(message, peerNodeAddress); } else { @@ -220,7 +220,7 @@ public class TradeManager { } } - private void initTrade(Trade trade, Coin fundsNeededForTrade) { + private void initTrade(Trade trade, boolean useSavingsWallet, Coin fundsNeededForTrade) { trade.init(p2PService, walletService, tradeWalletService, @@ -229,6 +229,7 @@ public class TradeManager { openOfferManager, user, keyRing, + useSavingsWallet, fundsNeededForTrade); } @@ -260,12 +261,13 @@ public class TradeManager { Coin fundsNeededForTrade, Offer offer, String paymentAccountId, + boolean useSavingsWallet, TradeResultHandler tradeResultHandler) { final OfferAvailabilityModel model = getOfferAvailabilityModel(offer); offer.checkOfferAvailability(model, () -> { if (offer.getState() == Offer.State.AVAILABLE) - createTrade(amount, fundsNeededForTrade, offer, paymentAccountId, model, tradeResultHandler); + createTrade(amount, fundsNeededForTrade, offer, paymentAccountId, useSavingsWallet, model, tradeResultHandler); }); } @@ -273,6 +275,7 @@ public class TradeManager { Coin fundsNeededForTrade, Offer offer, String paymentAccountId, + boolean useSavingsWallet, OfferAvailabilityModel model, TradeResultHandler tradeResultHandler) { Trade trade; @@ -285,7 +288,7 @@ public class TradeManager { trade.setTakeOfferDateAsBlockHeight(tradeWalletService.getBestChainHeight()); trade.setTakerPaymentAccountId(paymentAccountId); - initTrade(trade, fundsNeededForTrade); + initTrade(trade, useSavingsWallet, fundsNeededForTrade); trades.add(trade); ((TakerTrade) trade).takeAvailableOffer(); diff --git a/core/src/main/java/io/bitsquare/trade/offer/OpenOfferManager.java b/core/src/main/java/io/bitsquare/trade/offer/OpenOfferManager.java index 525c88f81c..b243257c43 100644 --- a/core/src/main/java/io/bitsquare/trade/offer/OpenOfferManager.java +++ b/core/src/main/java/io/bitsquare/trade/offer/OpenOfferManager.java @@ -141,8 +141,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe log.info("remove all open offers at shutDown"); // we remove own offers from offerbook when we go offline // Normally we use a delay for broadcasting to the peers, but at shut down we want to get it fast out - openOffers.forEach(openOffer -> offerBookService.removeOfferAtShutDown(openOffer.getOffer())); + closeAllOpenOffers(completeHandler); + } + public void closeAllOpenOffers(@Nullable Runnable completeHandler) { + openOffers.forEach(openOffer -> offerBookService.removeOfferAtShutDown(openOffer.getOffer())); if (completeHandler != null) UserThread.runAfter(completeHandler::run, openOffers.size() * 200 + 300, TimeUnit.MILLISECONDS); } diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/ProcessModel.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/ProcessModel.java index 9066443a35..700d5231f6 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/trade/ProcessModel.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/ProcessModel.java @@ -83,7 +83,7 @@ public class ProcessModel implements Model, Serializable { @Nullable private String changeOutputAddress; private Transaction takeOfferFeeTx; - public boolean useSavingsWallet; + private boolean useSavingsWallet; private Coin fundsNeededForTrade; public ProcessModel() { @@ -107,6 +107,7 @@ public class ProcessModel implements Model, Serializable { ArbitratorManager arbitratorManager, User user, KeyRing keyRing, + boolean useSavingsWallet, Coin fundsNeededForTrade) { this.offer = offer; this.tradeManager = tradeManager; @@ -117,6 +118,7 @@ public class ProcessModel implements Model, Serializable { this.user = user; this.keyRing = keyRing; this.p2PService = p2PService; + this.useSavingsWallet = useSavingsWallet; this.fundsNeededForTrade = fundsNeededForTrade; } @@ -291,4 +293,8 @@ public class ProcessModel implements Model, Serializable { public Transaction getTakeOfferFeeTx() { return takeOfferFeeTx; } + + public boolean getUseSavingsWallet() { + return useSavingsWallet; + } } diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/CreateTakeOfferFeeTx.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/CreateTakeOfferFeeTx.java index 4e4141f713..16b92b7199 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/CreateTakeOfferFeeTx.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/CreateTakeOfferFeeTx.java @@ -52,7 +52,7 @@ public class CreateTakeOfferFeeTx extends TradeTask { processModel.getAddressEntry(), processModel.getUnusedSavingsAddress(), processModel.getFundsNeededForTrade(), - processModel.useSavingsWallet, + processModel.getUseSavingsWallet(), FeePolicy.getTakeOfferFee(), selectedArbitrator.getBtcAddress()); diff --git a/gui/src/main/java/io/bitsquare/gui/components/BalanceTextField.java b/gui/src/main/java/io/bitsquare/gui/components/BalanceTextField.java index 1e23174c0e..11d1eaa4f0 100644 --- a/gui/src/main/java/io/bitsquare/gui/components/BalanceTextField.java +++ b/gui/src/main/java/io/bitsquare/gui/components/BalanceTextField.java @@ -34,6 +34,7 @@ public class BalanceTextField extends AnchorPane { private static WalletService walletService; private BalanceListener balanceListener; + private Coin targetAmount; public static void setWalletService(WalletService walletService) { BalanceTextField.walletService = walletService; @@ -84,6 +85,10 @@ public class BalanceTextField extends AnchorPane { updateBalance(balance); } + public void setTargetAmount(Coin targetAmount) { + this.targetAmount = targetAmount; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private methods /////////////////////////////////////////////////////////////////////////////////////////// @@ -91,10 +96,12 @@ public class BalanceTextField extends AnchorPane { private void updateBalance(Coin balance) { if (formatter != null) textField.setText(formatter.formatCoinWithCode(balance)); - if (balance.isPositive()) - textField.setEffect(fundedEffect); - else - textField.setEffect(notFundedEffect); + if (targetAmount != null) { + if (balance.compareTo(targetAmount) >= 0) + textField.setEffect(fundedEffect); + else + textField.setEffect(notFundedEffect); + } } } diff --git a/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java b/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java index b3f50b92c9..7fb9020ace 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java +++ b/gui/src/main/java/io/bitsquare/gui/main/MainViewModel.java @@ -454,9 +454,11 @@ public class MainViewModel implements ViewModel { updateBalance(); setupDevDummyPaymentAccount(); setupMarketPriceFeed(); + swapPendingTradeAddressEntriesToSavingsWallet(); showAppScreen.set(true); + // We want to test if the client is compiled with the correct crypto provider (BountyCastle) // and if the unlimited Strength for cryptographic keys is set. // If users compile themselves they might miss that step and then would get an exception in the trade. @@ -667,6 +669,12 @@ public class MainViewModel implements ViewModel { typeProperty.bind(priceFeed.typeProperty()); } + private void swapPendingTradeAddressEntriesToSavingsWallet() { + TradableCollections.getAddressEntriesForAvailableBalance(openOfferManager, tradeManager, walletService).stream() + .filter(addressEntry -> addressEntry.getOfferId() != null) + .forEach(addressEntry -> walletService.swapTradeToSavings(addressEntry.getOfferId())); + } + private void displayAlertIfPresent(Alert alert) { boolean alreadyDisplayed = alert != null && alert.equals(user.getDisplayedAlert()); user.setDisplayedAlert(alert); diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferDataModel.java b/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferDataModel.java index 34dfc257f3..c47e227cf6 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferDataModel.java +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferDataModel.java @@ -183,9 +183,6 @@ class CreateOfferDataModel extends ActivatableDataModel { calculateTotalToPay(); updateBalance(); - if (direction == Offer.Direction.BUY) - calculateTotalToPay(); - if (isTabSelected) priceFeed.setCurrencyCode(tradeCurrencyCode.get()); } @@ -402,7 +399,7 @@ class CreateOfferDataModel extends ActivatableDataModel { } void updateBalance() { - Coin tradeWalletBalance = walletService.getBalanceForAddress(getAddressEntry().getAddress()); + Coin tradeWalletBalance = walletService.getBalanceForAddress(addressEntry.getAddress()); if (useSavingsWallet) { Coin savingWalletBalance = walletService.getSavingWalletBalance(); totalAvailableBalance = savingWalletBalance.add(tradeWalletBalance); @@ -419,7 +416,7 @@ class CreateOfferDataModel extends ActivatableDataModel { isWalletFunded.set(isBalanceSufficient(balance.get())); if (isWalletFunded.get()) { - walletService.removeBalanceListener(balanceListener); + //walletService.removeBalanceListener(balanceListener); if (walletFundedNotification == null) { walletFundedNotification = new Notification() .headLine("Trading wallet update") @@ -457,6 +454,6 @@ class CreateOfferDataModel extends ActivatableDataModel { } public void swapTradeToSavings() { - walletService.swapTradeToSavings(getOfferId()); + walletService.swapTradeToSavings(offerId); } } diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferView.java b/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferView.java index 2c7e5dbda1..4f875b8628 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/createoffer/CreateOfferView.java @@ -177,6 +177,7 @@ public class CreateOfferView extends ActivatableViewAndModel amountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty minAmountValidationResult = new @@ -109,7 +110,6 @@ class CreateOfferViewModel extends ActivatableWithDataModel errorMessageListener; private Offer offer; private Timer timeoutTimer; - private boolean showPayFundsScreenDisplayed; /////////////////////////////////////////////////////////////////////////////////////////// @@ -391,7 +391,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel amountAsCoin = new SimpleObjectProperty<>(); final ObjectProperty volumeAsFiat = new SimpleObjectProperty<>(); final ObjectProperty totalToPayAsCoin = new SimpleObjectProperty<>(); - final ObjectProperty feeFromFundingTxProperty = new SimpleObjectProperty(Coin.NEGATIVE_SATOSHI); + final ObjectProperty balance = new SimpleObjectProperty<>(); + final ObjectProperty missingCoin = new SimpleObjectProperty<>(Coin.ZERO); private BalanceListener balanceListener; PaymentAccount paymentAccount; private boolean isTabSelected; + boolean useSavingsWallet; + Coin totalAvailableBalance; + private Notification walletFundedNotification; /////////////////////////////////////////////////////////////////////////////////////////// @@ -115,6 +123,8 @@ class TakeOfferDataModel extends ActivatableDataModel { takerFeeAsCoin = FeePolicy.getTakeOfferFee(); networkFeeAsCoin = FeePolicy.getFixedTxFeeForTrades(); securityDepositAsCoin = FeePolicy.getSecurityDeposit(); + + isMainNet.set(preferences.getBitcoinNetwork() == BitcoinNetwork.MAINNET); } @Override @@ -124,13 +134,15 @@ class TakeOfferDataModel extends ActivatableDataModel { addBindings(); addListeners(); - updateBalance(walletService.getBalanceForAddress(addressEntry.getAddress())); + + calculateTotalToPay(); + updateBalance(); // TODO In case that we have funded but restarted, or canceled but took again the offer we would need to // store locally the result when we received the funding tx(s). // For now we just ignore that rare case and bypass the check by setting a sufficient value - if (isWalletFunded.get()) - feeFromFundingTxProperty.set(FeePolicy.getMinRequiredFeeForFundingTx()); + // if (isWalletFunded.get()) + // feeFromFundingTxProperty.set(FeePolicy.getMinRequiredFeeForFundingTx()); if (isTabSelected) priceFeed.setCurrencyCode(offer.getCurrencyCode()); @@ -167,19 +179,24 @@ class TakeOfferDataModel extends ActivatableDataModel { if (BitsquareApp.DEV_MODE) amountAsCoin.set(offer.getAmount()); + calculateTotalToPay(); calculateVolume(); calculateTotalToPay(); balanceListener = new BalanceListener(addressEntry.getAddress()) { @Override public void onBalanceChanged(Coin balance, Transaction tx) { - updateBalance(balance); + updateBalance(); - if (preferences.getBitcoinNetwork() == BitcoinNetwork.MAINNET) { + if (tx.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) { + + } + + if (isMainNet.get()) { SettableFuture future = blockchainService.requestFee(tx.getHashAsString()); Futures.addCallback(future, new FutureCallback() { public void onSuccess(Coin fee) { - UserThread.execute(() -> feeFromFundingTxProperty.set(fee)); + UserThread.execute(() -> setFeeFromFundingTx(fee)); } public void onFailure(@NotNull Throwable throwable) { @@ -189,14 +206,15 @@ class TakeOfferDataModel extends ActivatableDataModel { "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())) + .onAction(() -> setFeeFromFundingTx(FeePolicy.getMinRequiredFeeForFundingTx())) .closeButtonText("No. Let's cancel that payment.") - .onClose(() -> feeFromFundingTxProperty.set(Coin.ZERO)) + .onClose(() -> setFeeFromFundingTx(Coin.NEGATIVE_SATOSHI)) .show()); } }); } else { - feeFromFundingTxProperty.set(FeePolicy.getMinRequiredFeeForFundingTx()); + setFeeFromFundingTx(FeePolicy.getMinRequiredFeeForFundingTx()); + isFeeFromFundingTxSufficient.set(feeFromFundingTx.compareTo(FeePolicy.getMinRequiredFeeForFundingTx()) >= 0); } } }; @@ -205,7 +223,6 @@ class TakeOfferDataModel extends ActivatableDataModel { priceFeed.setCurrencyCode(offer.getCurrencyCode()); } - void onTabSelected(boolean isSelected) { this.isTabSelected = isSelected; if (isTabSelected) @@ -235,6 +252,7 @@ class TakeOfferDataModel extends ActivatableDataModel { totalToPayAsCoin.get().subtract(takerFeeAsCoin), offer, paymentAccount.getId(), + useSavingsWallet, tradeResultHandler ); } @@ -244,6 +262,11 @@ class TakeOfferDataModel extends ActivatableDataModel { this.paymentAccount = paymentAccount; } + void useSavingsWalletForFunding() { + useSavingsWallet = true; + updateBalance(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters @@ -275,10 +298,6 @@ class TakeOfferDataModel extends ActivatableDataModel { return user.getAcceptedArbitrators().size() > 0; } - boolean isFeeFromFundingTxSufficient() { - return feeFromFundingTxProperty.get().compareTo(FeePolicy.getMinRequiredFeeForFundingTx()) >= 0; - } - /////////////////////////////////////////////////////////////////////////////////////////// // Bindings, listeners @@ -311,7 +330,7 @@ class TakeOfferDataModel extends ActivatableDataModel { !amountAsCoin.get().isZero()) { volumeAsFiat.set(new ExchangeRate(offer.getPrice()).coinToFiat(amountAsCoin.get())); - updateBalance(walletService.getBalanceForAddress(addressEntry.getAddress())); + updateBalance(); } } @@ -322,11 +341,49 @@ class TakeOfferDataModel extends ActivatableDataModel { totalToPayAsCoin.set(takerFeeAsCoin.add(networkFeeAsCoin).add(securityDepositAsCoin).add(amountAsCoin.get())); } - private void updateBalance(@NotNull Coin balance) { - isWalletFunded.set(totalToPayAsCoin.get() != null && balance.compareTo(totalToPayAsCoin.get()) >= 0); + void updateBalance() { + Coin tradeWalletBalance = walletService.getBalanceForAddress(addressEntry.getAddress()); + if (useSavingsWallet) { + Coin savingWalletBalance = walletService.getSavingWalletBalance(); + totalAvailableBalance = savingWalletBalance.add(tradeWalletBalance); - if (isWalletFunded.get()) - walletService.removeBalanceListener(balanceListener); + if (totalAvailableBalance.compareTo(totalToPayAsCoin.get()) > 0) + balance.set(totalToPayAsCoin.get()); + else + balance.set(totalAvailableBalance); + } else { + balance.set(tradeWalletBalance); + } + + missingCoin.set(totalToPayAsCoin.get().subtract(balance.get())); + + isWalletFunded.set(isBalanceSufficient(balance.get())); + if (isWalletFunded.get()) { + // walletService.removeBalanceListener(balanceListener); + if (walletFundedNotification == null) { + walletFundedNotification = new Notification() + .headLine("Trading wallet update") + .notification("Your trading wallet is sufficiently funded.\n" + + "Amount: " + formatter.formatCoinWithCode(totalToPayAsCoin.get())) + .autoClose(); + + walletFundedNotification.show(); + } + } + } + + private boolean isBalanceSufficient(Coin balance) { + return totalToPayAsCoin.get() != null && balance.compareTo(totalToPayAsCoin.get()) >= 0; + } + + public void swapTradeToSavings() { + walletService.swapTradeToSavings(offer.getId()); + setFeeFromFundingTx(Coin.NEGATIVE_SATOSHI); + } + + private void setFeeFromFundingTx(Coin fee) { + feeFromFundingTx = fee; + isFeeFromFundingTxSufficient.set(feeFromFundingTx.compareTo(FeePolicy.getMinRequiredFeeForFundingTx()) >= 0); } boolean isMinAmountLessOrEqualAmount() { diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferView.java b/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferView.java index bac714577b..3633765a90 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferView.java @@ -60,15 +60,16 @@ import javafx.stage.Window; import javafx.util.StringConverter; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; -import org.bitcoinj.core.Coin; import org.bitcoinj.uri.BitcoinURI; import org.controlsfx.control.PopOver; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; +import org.fxmisc.easybind.monadic.MonadicBinding; import org.jetbrains.annotations.NotNull; import javax.inject.Inject; import java.io.ByteArrayInputStream; +import java.net.URI; import java.util.concurrent.TimeUnit; import static io.bitsquare.gui.util.FormBuilder.*; @@ -89,7 +90,7 @@ public class TakeOfferView extends ActivatableViewAndModel feeFromFundingTxListener; private boolean offerDetailsWindowDisplayed; private Notification walletFundedNotification; - private Subscription isWalletFundedSubscription; private ImageView qrCodeImageView; + private HBox fundingHBox; + private Subscription balanceSubscription; + private Subscription noSufficientFeeSubscription; + private MonadicBinding noSufficientFeeBinding; + private Subscription totalToPaySubscription; /////////////////////////////////////////////////////////////////////////////////////////// @@ -155,12 +159,19 @@ public class TakeOfferView extends ActivatableViewAndModel @@ -256,14 +267,15 @@ public class TakeOfferView extends ActivatableViewAndModel { - log.debug("feeFromFundingTxListener " + newValue); - if (!model.dataModel.isFeeFromFundingTxSufficient()) { + noSufficientFeeBinding = EasyBind.combine(model.dataModel.isWalletFunded, model.dataModel.isMainNet, model.dataModel.isFeeFromFundingTxSufficient, + (isWalletFunded, isMainNet, isFeeSufficient) -> isWalletFunded && isMainNet && !isFeeSufficient); + noSufficientFeeSubscription = noSufficientFeeBinding.subscribe((observable, oldValue, newValue) -> { + if (newValue) new Popup().warning("The mining fee from your funding transaction is not sufficiently high.\n\n" + "You need to use at least a mining fee of " + model.formatter.formatCoinWithCode(FeePolicy.getMinRequiredFeeForFundingTx()) + ".\n\n" + "The fee used in your funding transaction was only " + - model.formatter.formatCoinWithCode(newValue) + ".\n\n" + + model.formatter.formatCoinWithCode(model.dataModel.feeFromFundingTx) + ".\n\n" + "The trade transactions might take too much time to be included in " + "a block if the fee is too low.\n" + "Please check at your external wallet that you set the required fee and " + @@ -275,15 +287,16 @@ public class TakeOfferView extends ActivatableViewAndModel balanceTextField.setBalance(newValue)); + totalToPaySubscription = EasyBind.subscribe(model.dataModel.totalToPayAsCoin, newValue -> balanceTextField.setTargetAmount(newValue)); } @Override @@ -298,16 +311,23 @@ public class TakeOfferView extends ActivatableViewAndModel navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class)) + .show(); + } + // TODO need other implementation as it is displayed also if there are old funds in the wallet /* if (model.dataModel.isWalletFunded.get()) @@ -461,7 +490,6 @@ public class TakeOfferView extends ActivatableViewAndModel { - if (isFunded) { - if (walletFundedNotification == null) { - walletFundedNotification = new Notification() - .headLine("Trading wallet update") - .notification("Your trading wallet is sufficiently funded.\n" + - "Amount: " + formatter.formatCoinWithCode(model.dataModel.totalToPayAsCoin.get())) - .autoClose(); - walletFundedNotification.show(); - } - } - }); } final byte[] imageBytes = QRCode @@ -689,9 +704,41 @@ public class TakeOfferView extends ActivatableViewAndModel takeOfferTuple = addButtonWithStatusAfterGroup(gridPane, ++gridRow, ""); - takeOfferButton = takeOfferTuple.first; + fundingHBox = new HBox(); + fundingHBox.setVisible(false); + fundingHBox.setManaged(false); + fundingHBox.setSpacing(10); + fundFromSavingsWalletButton = new Button("Transfer funds from Bitsquare wallet"); + fundFromSavingsWalletButton.setDefaultButton(true); + fundFromSavingsWalletButton.setDefaultButton(false); + fundFromSavingsWalletButton.setOnAction(e -> model.useSavingsWalletForFunding()); + Label label = new Label("OR"); + label.setPadding(new Insets(5, 0, 0, 0)); + fundFromExternalWalletButton = new Button("Pay in funds from external wallet"); + fundFromExternalWalletButton.setDefaultButton(false); + fundFromExternalWalletButton.setOnAction(e -> { + try { + Utilities.openURI(URI.create(getBitcoinURI())); + } catch (Exception ex) { + log.warn(ex.getMessage()); + new Popup().warning("Opening a default bitcoin wallet application has failed. " + + "Perhaps you don't have one installed?").show(); + } + }); + spinner = new ProgressIndicator(0); + spinner.setPrefHeight(18); + spinner.setPrefWidth(18); + spinnerInfoLabel = new Label(); + spinnerInfoLabel.setPadding(new Insets(5, 0, 0, 0)); + fundingHBox.getChildren().addAll(fundFromSavingsWalletButton, label, fundFromExternalWalletButton, spinner, spinnerInfoLabel); + GridPane.setRowIndex(fundingHBox, ++gridRow); + GridPane.setColumnIndex(fundingHBox, 1); + GridPane.setMargin(fundingHBox, new Insets(15, 10, 0, 0)); + gridPane.getChildren().add(fundingHBox); + + takeOfferButton = addButtonAfterGroup(gridPane, gridRow, ""); takeOfferButton.setVisible(false); + takeOfferButton.setManaged(false); takeOfferButton.setMinHeight(40); takeOfferButton.setPadding(new Insets(0, 20, 0, 20)); takeOfferButton.setOnAction(e -> { @@ -699,9 +746,6 @@ public class TakeOfferView extends ActivatableViewAndModel { if (model.dataModel.isWalletFunded.get()) @@ -716,12 +760,11 @@ public class TakeOfferView extends ActivatableViewAndModel implements ViewModel { + final TakeOfferDataModel dataModel; private final BtcValidator btcValidator; private final P2PService p2PService; + private final Navigation navigation; final BSFormatter formatter; private String amountRange; @@ -75,11 +83,12 @@ class TakeOfferViewModel extends ActivatableWithDataModel im final BooleanProperty isSpinnerVisible = new SimpleBooleanProperty(); final BooleanProperty showWarningInvalidBtcDecimalPlaces = new SimpleBooleanProperty(); final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); + final BooleanProperty takeOfferCompleted = new SimpleBooleanProperty(); + final BooleanProperty showPayFundsScreenDisplayed = new SimpleBooleanProperty(); final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(); // Those are needed for the addressTextField - final ObjectProperty totalToPayAsCoin = new SimpleObjectProperty<>(); final ObjectProperty
address = new SimpleObjectProperty<>(); private ChangeListener amountListener; @@ -90,9 +99,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel im private ChangeListener offerStateListener; private ChangeListener offerErrorListener; private ConnectionListener connectionListener; - private ChangeListener feeFromFundingTxListener; + private Subscription isFeeSufficientSubscription; private Runnable takeOfferSucceededHandler; - private boolean showPayFundsScreenDisplayed; /////////////////////////////////////////////////////////////////////////////////////////// @@ -101,11 +109,13 @@ class TakeOfferViewModel extends ActivatableWithDataModel im @Inject public TakeOfferViewModel(TakeOfferDataModel dataModel, BtcValidator btcValidator, P2PService p2PService, - BSFormatter formatter) { + Navigation navigation, BSFormatter formatter) { super(dataModel); + this.dataModel = dataModel; this.btcValidator = btcValidator; this.p2PService = p2PService; + this.navigation = navigation; this.formatter = formatter; createListeners(); @@ -190,6 +200,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im trade.errorMessageProperty().addListener(tradeErrorListener); applyTradeErrorMessage(trade.errorMessageProperty().get()); updateButtonDisableState(); + takeOfferCompleted.set(true); }); } @@ -200,10 +211,30 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } public void onShowPayFundsScreen() { - showPayFundsScreenDisplayed = true; + showPayFundsScreenDisplayed.set(true); updateSpinnerInfo(); } + boolean useSavingsWalletForFunding() { + dataModel.useSavingsWalletForFunding(); + 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/Depost funds\"") + .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) + .show(); + return false; + } + + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Handle focus /////////////////////////////////////////////////////////////////////////////////////////// @@ -286,10 +317,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im break; } - if (offerWarning.get() != null) { - isSpinnerVisible.set(false); - spinnerInfoText.set(""); - } + updateSpinnerInfo(); updateButtonDisableState(); } @@ -328,8 +356,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } this.errorMessage.set(errorMessage + appendMsg); - isSpinnerVisible.set(false); - spinnerInfoText.set(""); + updateSpinnerInfo(); if (takeOfferSucceededHandler != null) takeOfferSucceededHandler.run(); @@ -349,9 +376,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel im if (takeOfferSucceededHandler != null) takeOfferSucceededHandler.run(); - isSpinnerVisible.set(false); - spinnerInfoText.set(""); showTransactionPublishedScreen.set(true); + updateSpinnerInfo(); } else { log.error("trade.getDepositTx() == null. That must not happen"); } @@ -364,10 +390,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel im && !dataModel.isAmountLargerThanOfferAmount() && isOfferAvailable.get(); isNextButtonDisabled.set(!inputDataValid); - isTakeOfferButtonDisabled.set(!(inputDataValid - && dataModel.isWalletFunded.get() - && !takeOfferRequested - && dataModel.isFeeFromFundingTxSufficient())); + boolean notSufficientFees = dataModel.isWalletFunded.get() && dataModel.isMainNet.get() && !dataModel.isFeeFromFundingTxSufficient.get(); + isTakeOfferButtonDisabled.set(takeOfferRequested || !inputDataValid || notSufficientFees); } @@ -385,7 +409,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel im volumeDescriptionLabel.set(BSResources.get("createOffer.amountPriceBox.sell.volumeDescription", dataModel.getCurrencyCode())); } totalToPay.bind(createStringBinding(() -> formatter.formatCoinWithCode(dataModel.totalToPayAsCoin.get()), dataModel.totalToPayAsCoin)); - totalToPayAsCoin.bind(dataModel.totalToPayAsCoin); btcCode.bind(dataModel.btcCode); } @@ -394,7 +417,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel im volumeDescriptionLabel.unbind(); volume.unbind(); totalToPay.unbind(); - totalToPayAsCoin.unbind(); btcCode.unbind(); } @@ -410,16 +432,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel im amountAsCoinListener = (ov, oldValue, newValue) -> amount.set(formatter.formatCoin(newValue)); isWalletFundedListener = (ov, oldValue, newValue) -> { updateButtonDisableState(); - isSpinnerVisible.set(true); - spinnerInfoText.set("Checking funding tx miner fee..."); - }; - feeFromFundingTxListener = (ov, oldValue, newValue) -> { - updateButtonDisableState(); - if (newValue.compareTo(FeePolicy.getMinRequiredFeeForFundingTx()) >= 0) { - isSpinnerVisible.set(false); - spinnerInfoText.set(""); - } }; + tradeStateListener = (ov, oldValue, newValue) -> applyTradeState(newValue); tradeErrorListener = (ov, oldValue, newValue) -> applyTradeErrorMessage(newValue); offerStateListener = (ov, oldValue, newValue) -> applyOfferState(newValue); @@ -432,8 +446,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im "He might have gone offline or has closed the connection to you because of too " + "many open connections.\n\n" + "If you can still see his offer in the offerbook you can try to take the offer again."); - isSpinnerVisible.set(false); - spinnerInfoText.set(""); + updateSpinnerInfo(); } } @@ -448,15 +461,23 @@ class TakeOfferViewModel extends ActivatableWithDataModel im } private void updateSpinnerInfo() { - if (dataModel.isWalletFunded.get() || !showPayFundsScreenDisplayed) { - isSpinnerVisible.set(false); + if (!showPayFundsScreenDisplayed.get() || + offerWarning.get() != null || + errorMessage.get() != null || + showTransactionPublishedScreen.get()) { spinnerInfoText.set(""); - } else if (showPayFundsScreenDisplayed) { - spinnerInfoText.set("Waiting for receiving funds..."); - isSpinnerVisible.set(true); + } else if (dataModel.isWalletFunded.get()) { + if (dataModel.isFeeFromFundingTxSufficient.get()) { + spinnerInfoText.set(""); + } else { + spinnerInfoText.set("Check if funding tx miner fee is sufficient..."); + } + } else { + spinnerInfoText.set("Waiting for funds..."); } - } + isSpinnerVisible.set(!spinnerInfoText.get().isEmpty()); + } private void addListeners() { // Bidirectional bindings are used for all input fields: amount, price, volume and minAmount @@ -468,7 +489,10 @@ class TakeOfferViewModel extends ActivatableWithDataModel im dataModel.isWalletFunded.addListener(isWalletFundedListener); p2PService.getNetworkNode().addConnectionListener(connectionListener); - dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener); + isFeeSufficientSubscription = EasyBind.subscribe(dataModel.isFeeFromFundingTxSufficient, newValue -> { + updateButtonDisableState(); + updateSpinnerInfo(); + }); } private void removeListeners() { @@ -488,7 +512,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel im trade.errorMessageProperty().removeListener(tradeErrorListener); } p2PService.getNetworkNode().removeConnectionListener(connectionListener); - dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); + isFeeSufficientSubscription.unsubscribe(); }