Savings wallet (WIP)

This commit is contained in:
Manfred Karrer 2016-03-31 19:29:54 +02:00
parent d0e4792094
commit 04845e4382
13 changed files with 280 additions and 127 deletions

View file

@ -221,6 +221,7 @@ public abstract class Trade implements Tradable, Model {
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
User user, User user,
KeyRing keyRing, KeyRing keyRing,
boolean useSavingsWallet,
Coin fundsNeededForTrade) { Coin fundsNeededForTrade) {
Log.traceCall(); Log.traceCall();
processModel.onAllServicesInitialized(offer, processModel.onAllServicesInitialized(offer,
@ -232,6 +233,7 @@ public abstract class Trade implements Tradable, Model {
arbitratorManager, arbitratorManager,
user, user,
keyRing, keyRing,
useSavingsWallet,
fundsNeededForTrade); fundsNeededForTrade);
createProtocol(); createProtocol();

View file

@ -178,7 +178,7 @@ public class TradeManager {
else {*/ else {*/
trade.setStorage(tradableListStorage); trade.setStorage(tradableListStorage);
trade.updateDepositTxFromWallet(tradeWalletService); 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 = new SellerAsOffererTrade(offer, tradableListStorage);
trade.setStorage(tradableListStorage); trade.setStorage(tradableListStorage);
initTrade(trade, trade.getProcessModel().getFundsNeededForTrade()); initTrade(trade, trade.getProcessModel().getUseSavingsWallet(), trade.getProcessModel().getFundsNeededForTrade());
trades.add(trade); trades.add(trade);
((OffererTrade) trade).handleTakeOfferRequest(message, peerNodeAddress); ((OffererTrade) trade).handleTakeOfferRequest(message, peerNodeAddress);
} else { } 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, trade.init(p2PService,
walletService, walletService,
tradeWalletService, tradeWalletService,
@ -229,6 +229,7 @@ public class TradeManager {
openOfferManager, openOfferManager,
user, user,
keyRing, keyRing,
useSavingsWallet,
fundsNeededForTrade); fundsNeededForTrade);
} }
@ -260,12 +261,13 @@ public class TradeManager {
Coin fundsNeededForTrade, Coin fundsNeededForTrade,
Offer offer, Offer offer,
String paymentAccountId, String paymentAccountId,
boolean useSavingsWallet,
TradeResultHandler tradeResultHandler) { TradeResultHandler tradeResultHandler) {
final OfferAvailabilityModel model = getOfferAvailabilityModel(offer); final OfferAvailabilityModel model = getOfferAvailabilityModel(offer);
offer.checkOfferAvailability(model, offer.checkOfferAvailability(model,
() -> { () -> {
if (offer.getState() == Offer.State.AVAILABLE) 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, Coin fundsNeededForTrade,
Offer offer, Offer offer,
String paymentAccountId, String paymentAccountId,
boolean useSavingsWallet,
OfferAvailabilityModel model, OfferAvailabilityModel model,
TradeResultHandler tradeResultHandler) { TradeResultHandler tradeResultHandler) {
Trade trade; Trade trade;
@ -285,7 +288,7 @@ public class TradeManager {
trade.setTakeOfferDateAsBlockHeight(tradeWalletService.getBestChainHeight()); trade.setTakeOfferDateAsBlockHeight(tradeWalletService.getBestChainHeight());
trade.setTakerPaymentAccountId(paymentAccountId); trade.setTakerPaymentAccountId(paymentAccountId);
initTrade(trade, fundsNeededForTrade); initTrade(trade, useSavingsWallet, fundsNeededForTrade);
trades.add(trade); trades.add(trade);
((TakerTrade) trade).takeAvailableOffer(); ((TakerTrade) trade).takeAvailableOffer();

View file

@ -141,8 +141,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
log.info("remove all open offers at shutDown"); log.info("remove all open offers at shutDown");
// we remove own offers from offerbook when we go offline // 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 // 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) if (completeHandler != null)
UserThread.runAfter(completeHandler::run, openOffers.size() * 200 + 300, TimeUnit.MILLISECONDS); UserThread.runAfter(completeHandler::run, openOffers.size() * 200 + 300, TimeUnit.MILLISECONDS);
} }

View file

@ -83,7 +83,7 @@ public class ProcessModel implements Model, Serializable {
@Nullable @Nullable
private String changeOutputAddress; private String changeOutputAddress;
private Transaction takeOfferFeeTx; private Transaction takeOfferFeeTx;
public boolean useSavingsWallet; private boolean useSavingsWallet;
private Coin fundsNeededForTrade; private Coin fundsNeededForTrade;
public ProcessModel() { public ProcessModel() {
@ -107,6 +107,7 @@ public class ProcessModel implements Model, Serializable {
ArbitratorManager arbitratorManager, ArbitratorManager arbitratorManager,
User user, User user,
KeyRing keyRing, KeyRing keyRing,
boolean useSavingsWallet,
Coin fundsNeededForTrade) { Coin fundsNeededForTrade) {
this.offer = offer; this.offer = offer;
this.tradeManager = tradeManager; this.tradeManager = tradeManager;
@ -117,6 +118,7 @@ public class ProcessModel implements Model, Serializable {
this.user = user; this.user = user;
this.keyRing = keyRing; this.keyRing = keyRing;
this.p2PService = p2PService; this.p2PService = p2PService;
this.useSavingsWallet = useSavingsWallet;
this.fundsNeededForTrade = fundsNeededForTrade; this.fundsNeededForTrade = fundsNeededForTrade;
} }
@ -291,4 +293,8 @@ public class ProcessModel implements Model, Serializable {
public Transaction getTakeOfferFeeTx() { public Transaction getTakeOfferFeeTx() {
return takeOfferFeeTx; return takeOfferFeeTx;
} }
public boolean getUseSavingsWallet() {
return useSavingsWallet;
}
} }

View file

@ -52,7 +52,7 @@ public class CreateTakeOfferFeeTx extends TradeTask {
processModel.getAddressEntry(), processModel.getAddressEntry(),
processModel.getUnusedSavingsAddress(), processModel.getUnusedSavingsAddress(),
processModel.getFundsNeededForTrade(), processModel.getFundsNeededForTrade(),
processModel.useSavingsWallet, processModel.getUseSavingsWallet(),
FeePolicy.getTakeOfferFee(), FeePolicy.getTakeOfferFee(),
selectedArbitrator.getBtcAddress()); selectedArbitrator.getBtcAddress());

View file

@ -34,6 +34,7 @@ public class BalanceTextField extends AnchorPane {
private static WalletService walletService; private static WalletService walletService;
private BalanceListener balanceListener; private BalanceListener balanceListener;
private Coin targetAmount;
public static void setWalletService(WalletService walletService) { public static void setWalletService(WalletService walletService) {
BalanceTextField.walletService = walletService; BalanceTextField.walletService = walletService;
@ -84,6 +85,10 @@ public class BalanceTextField extends AnchorPane {
updateBalance(balance); updateBalance(balance);
} }
public void setTargetAmount(Coin targetAmount) {
this.targetAmount = targetAmount;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Private methods // Private methods
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -91,10 +96,12 @@ public class BalanceTextField extends AnchorPane {
private void updateBalance(Coin balance) { private void updateBalance(Coin balance) {
if (formatter != null) if (formatter != null)
textField.setText(formatter.formatCoinWithCode(balance)); textField.setText(formatter.formatCoinWithCode(balance));
if (balance.isPositive()) if (targetAmount != null) {
textField.setEffect(fundedEffect); if (balance.compareTo(targetAmount) >= 0)
else textField.setEffect(fundedEffect);
textField.setEffect(notFundedEffect); else
textField.setEffect(notFundedEffect);
}
} }
} }

View file

@ -454,9 +454,11 @@ public class MainViewModel implements ViewModel {
updateBalance(); updateBalance();
setupDevDummyPaymentAccount(); setupDevDummyPaymentAccount();
setupMarketPriceFeed(); setupMarketPriceFeed();
swapPendingTradeAddressEntriesToSavingsWallet();
showAppScreen.set(true); showAppScreen.set(true);
// We want to test if the client is compiled with the correct crypto provider (BountyCastle) // 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. // 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. // 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()); 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) { private void displayAlertIfPresent(Alert alert) {
boolean alreadyDisplayed = alert != null && alert.equals(user.getDisplayedAlert()); boolean alreadyDisplayed = alert != null && alert.equals(user.getDisplayedAlert());
user.setDisplayedAlert(alert); user.setDisplayedAlert(alert);

View file

@ -183,9 +183,6 @@ class CreateOfferDataModel extends ActivatableDataModel {
calculateTotalToPay(); calculateTotalToPay();
updateBalance(); updateBalance();
if (direction == Offer.Direction.BUY)
calculateTotalToPay();
if (isTabSelected) if (isTabSelected)
priceFeed.setCurrencyCode(tradeCurrencyCode.get()); priceFeed.setCurrencyCode(tradeCurrencyCode.get());
} }
@ -402,7 +399,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
} }
void updateBalance() { void updateBalance() {
Coin tradeWalletBalance = walletService.getBalanceForAddress(getAddressEntry().getAddress()); Coin tradeWalletBalance = walletService.getBalanceForAddress(addressEntry.getAddress());
if (useSavingsWallet) { if (useSavingsWallet) {
Coin savingWalletBalance = walletService.getSavingWalletBalance(); Coin savingWalletBalance = walletService.getSavingWalletBalance();
totalAvailableBalance = savingWalletBalance.add(tradeWalletBalance); totalAvailableBalance = savingWalletBalance.add(tradeWalletBalance);
@ -419,7 +416,7 @@ class CreateOfferDataModel extends ActivatableDataModel {
isWalletFunded.set(isBalanceSufficient(balance.get())); isWalletFunded.set(isBalanceSufficient(balance.get()));
if (isWalletFunded.get()) { if (isWalletFunded.get()) {
walletService.removeBalanceListener(balanceListener); //walletService.removeBalanceListener(balanceListener);
if (walletFundedNotification == null) { if (walletFundedNotification == null) {
walletFundedNotification = new Notification() walletFundedNotification = new Notification()
.headLine("Trading wallet update") .headLine("Trading wallet update")
@ -457,6 +454,6 @@ class CreateOfferDataModel extends ActivatableDataModel {
} }
public void swapTradeToSavings() { public void swapTradeToSavings() {
walletService.swapTradeToSavings(getOfferId()); walletService.swapTradeToSavings(offerId);
} }
} }

View file

@ -177,6 +177,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
onPaymentAccountsComboBoxSelected(); onPaymentAccountsComboBoxSelected();
balanceTextField.setBalance(model.dataModel.balance.get()); balanceTextField.setBalance(model.dataModel.balance.get());
balanceTextField.setTargetAmount(model.dataModel.totalToPayAsCoin.get());
} }
@Override @Override
@ -268,9 +269,8 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
currencyComboBox.setMouseTransparent(true); currencyComboBox.setMouseTransparent(true);
paymentAccountsComboBox.setMouseTransparent(true); paymentAccountsComboBox.setMouseTransparent(true);
fundingHBox.visibleProperty().bind(model.dataModel.isWalletFunded.not()); balanceTextField.setTargetAmount(model.dataModel.totalToPayAsCoin.get());
fundingHBox.managedProperty().bind(model.dataModel.isWalletFunded.not());
if (!BitsquareApp.DEV_MODE) { if (!BitsquareApp.DEV_MODE) {
String key = "securityDepositInfo"; String key = "securityDepositInfo";
new Popup().backgroundInfo("To ensure that both traders follow the trade protocol they need to pay a security deposit.\n\n" + new Popup().backgroundInfo("To ensure that both traders follow the trade protocol they need to pay a security deposit.\n\n" +
@ -405,8 +405,11 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
volumeTextField.validationResultProperty().bind(model.volumeValidationResult); volumeTextField.validationResultProperty().bind(model.volumeValidationResult);
// buttons // buttons
placeOfferButton.visibleProperty().bind(model.dataModel.isWalletFunded); fundingHBox.visibleProperty().bind(model.dataModel.isWalletFunded.not().and(model.showPayFundsScreenDisplayed));
placeOfferButton.managedProperty().bind(model.dataModel.isWalletFunded); fundingHBox.managedProperty().bind(model.dataModel.isWalletFunded.not().and(model.showPayFundsScreenDisplayed));
placeOfferButton.visibleProperty().bind(model.dataModel.isWalletFunded.and(model.showPayFundsScreenDisplayed));
placeOfferButton.managedProperty().bind(model.dataModel.isWalletFunded.and(model.showPayFundsScreenDisplayed));
placeOfferButton.disableProperty().bind(model.isPlaceOfferButtonDisabled); placeOfferButton.disableProperty().bind(model.isPlaceOfferButtonDisabled);
cancelButton2.disableProperty().bind(model.cancelButtonDisabled); cancelButton2.disableProperty().bind(model.cancelButtonDisabled);

View file

@ -86,6 +86,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
final BooleanProperty showWarningInvalidFiatDecimalPlaces = new SimpleBooleanProperty(); final BooleanProperty showWarningInvalidFiatDecimalPlaces = new SimpleBooleanProperty();
final BooleanProperty showWarningInvalidBtcDecimalPlaces = new SimpleBooleanProperty(); final BooleanProperty showWarningInvalidBtcDecimalPlaces = new SimpleBooleanProperty();
final BooleanProperty placeOfferCompleted = new SimpleBooleanProperty(); final BooleanProperty placeOfferCompleted = new SimpleBooleanProperty();
final BooleanProperty showPayFundsScreenDisplayed = new SimpleBooleanProperty();
final ObjectProperty<InputValidator.ValidationResult> amountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty<InputValidator.ValidationResult> amountValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<InputValidator.ValidationResult> minAmountValidationResult = new final ObjectProperty<InputValidator.ValidationResult> minAmountValidationResult = new
@ -109,7 +110,6 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
private ChangeListener<String> errorMessageListener; private ChangeListener<String> errorMessageListener;
private Offer offer; private Offer offer;
private Timer timeoutTimer; private Timer timeoutTimer;
private boolean showPayFundsScreenDisplayed;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -391,7 +391,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
} }
void onShowPayFundsScreen() { void onShowPayFundsScreen() {
showPayFundsScreenDisplayed = true; showPayFundsScreenDisplayed.set(true);
} }
boolean useSavingsWalletForFunding() { boolean useSavingsWalletForFunding() {

View file

@ -29,6 +29,7 @@ import io.bitsquare.btc.listeners.BalanceListener;
import io.bitsquare.btc.pricefeed.PriceFeed; import io.bitsquare.btc.pricefeed.PriceFeed;
import io.bitsquare.common.UserThread; import io.bitsquare.common.UserThread;
import io.bitsquare.gui.common.model.ActivatableDataModel; import io.bitsquare.gui.common.model.ActivatableDataModel;
import io.bitsquare.gui.main.overlays.notifications.Notification;
import io.bitsquare.gui.main.overlays.popups.Popup; import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.main.overlays.windows.WalletPasswordWindow; import io.bitsquare.gui.main.overlays.windows.WalletPasswordWindow;
import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.BSFormatter;
@ -46,6 +47,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction; import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.utils.ExchangeRate; import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat; import org.bitcoinj.utils.Fiat;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -75,21 +77,27 @@ class TakeOfferDataModel extends ActivatableDataModel {
private final Coin takerFeeAsCoin; private final Coin takerFeeAsCoin;
private final Coin networkFeeAsCoin; private final Coin networkFeeAsCoin;
private final Coin securityDepositAsCoin; private final Coin securityDepositAsCoin;
Coin feeFromFundingTx = Coin.NEGATIVE_SATOSHI;
private Offer offer; private Offer offer;
private AddressEntry addressEntry;
private AddressEntry addressEntry;
final StringProperty btcCode = new SimpleStringProperty(); final StringProperty btcCode = new SimpleStringProperty();
final BooleanProperty useMBTC = new SimpleBooleanProperty();
final BooleanProperty isWalletFunded = new SimpleBooleanProperty(); final BooleanProperty isWalletFunded = new SimpleBooleanProperty();
final BooleanProperty isFeeFromFundingTxSufficient = new SimpleBooleanProperty();
final BooleanProperty isMainNet = new SimpleBooleanProperty();
final ObjectProperty<Coin> amountAsCoin = new SimpleObjectProperty<>(); final ObjectProperty<Coin> amountAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Fiat> volumeAsFiat = new SimpleObjectProperty<>(); final ObjectProperty<Fiat> volumeAsFiat = new SimpleObjectProperty<>();
final ObjectProperty<Coin> totalToPayAsCoin = new SimpleObjectProperty<>(); final ObjectProperty<Coin> totalToPayAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Coin> feeFromFundingTxProperty = new SimpleObjectProperty(Coin.NEGATIVE_SATOSHI); final ObjectProperty<Coin> balance = new SimpleObjectProperty<>();
final ObjectProperty<Coin> missingCoin = new SimpleObjectProperty<>(Coin.ZERO);
private BalanceListener balanceListener; private BalanceListener balanceListener;
PaymentAccount paymentAccount; PaymentAccount paymentAccount;
private boolean isTabSelected; private boolean isTabSelected;
boolean useSavingsWallet;
Coin totalAvailableBalance;
private Notification walletFundedNotification;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -115,6 +123,8 @@ class TakeOfferDataModel extends ActivatableDataModel {
takerFeeAsCoin = FeePolicy.getTakeOfferFee(); takerFeeAsCoin = FeePolicy.getTakeOfferFee();
networkFeeAsCoin = FeePolicy.getFixedTxFeeForTrades(); networkFeeAsCoin = FeePolicy.getFixedTxFeeForTrades();
securityDepositAsCoin = FeePolicy.getSecurityDeposit(); securityDepositAsCoin = FeePolicy.getSecurityDeposit();
isMainNet.set(preferences.getBitcoinNetwork() == BitcoinNetwork.MAINNET);
} }
@Override @Override
@ -124,13 +134,15 @@ class TakeOfferDataModel extends ActivatableDataModel {
addBindings(); addBindings();
addListeners(); 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 // 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). // 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 // For now we just ignore that rare case and bypass the check by setting a sufficient value
if (isWalletFunded.get()) // if (isWalletFunded.get())
feeFromFundingTxProperty.set(FeePolicy.getMinRequiredFeeForFundingTx()); // feeFromFundingTxProperty.set(FeePolicy.getMinRequiredFeeForFundingTx());
if (isTabSelected) if (isTabSelected)
priceFeed.setCurrencyCode(offer.getCurrencyCode()); priceFeed.setCurrencyCode(offer.getCurrencyCode());
@ -167,19 +179,24 @@ class TakeOfferDataModel extends ActivatableDataModel {
if (BitsquareApp.DEV_MODE) if (BitsquareApp.DEV_MODE)
amountAsCoin.set(offer.getAmount()); amountAsCoin.set(offer.getAmount());
calculateTotalToPay();
calculateVolume(); calculateVolume();
calculateTotalToPay(); calculateTotalToPay();
balanceListener = new BalanceListener(addressEntry.getAddress()) { balanceListener = new BalanceListener(addressEntry.getAddress()) {
@Override @Override
public void onBalanceChanged(Coin balance, Transaction tx) { 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<Coin> future = blockchainService.requestFee(tx.getHashAsString()); SettableFuture<Coin> future = blockchainService.requestFee(tx.getHashAsString());
Futures.addCallback(future, new FutureCallback<Coin>() { Futures.addCallback(future, new FutureCallback<Coin>() {
public void onSuccess(Coin fee) { public void onSuccess(Coin fee) {
UserThread.execute(() -> feeFromFundingTxProperty.set(fee)); UserThread.execute(() -> setFeeFromFundingTx(fee));
} }
public void onFailure(@NotNull Throwable throwable) { 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 " + "Are you sure you used a sufficiently high fee of at least " +
formatter.formatCoinWithCode(FeePolicy.getMinRequiredFeeForFundingTx()) + "?") formatter.formatCoinWithCode(FeePolicy.getMinRequiredFeeForFundingTx()) + "?")
.actionButtonText("Yes, I used a sufficiently high fee.") .actionButtonText("Yes, I used a sufficiently high fee.")
.onAction(() -> feeFromFundingTxProperty.set(FeePolicy.getMinRequiredFeeForFundingTx())) .onAction(() -> setFeeFromFundingTx(FeePolicy.getMinRequiredFeeForFundingTx()))
.closeButtonText("No. Let's cancel that payment.") .closeButtonText("No. Let's cancel that payment.")
.onClose(() -> feeFromFundingTxProperty.set(Coin.ZERO)) .onClose(() -> setFeeFromFundingTx(Coin.NEGATIVE_SATOSHI))
.show()); .show());
} }
}); });
} else { } 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()); priceFeed.setCurrencyCode(offer.getCurrencyCode());
} }
void onTabSelected(boolean isSelected) { void onTabSelected(boolean isSelected) {
this.isTabSelected = isSelected; this.isTabSelected = isSelected;
if (isTabSelected) if (isTabSelected)
@ -235,6 +252,7 @@ class TakeOfferDataModel extends ActivatableDataModel {
totalToPayAsCoin.get().subtract(takerFeeAsCoin), totalToPayAsCoin.get().subtract(takerFeeAsCoin),
offer, offer,
paymentAccount.getId(), paymentAccount.getId(),
useSavingsWallet,
tradeResultHandler tradeResultHandler
); );
} }
@ -244,6 +262,11 @@ class TakeOfferDataModel extends ActivatableDataModel {
this.paymentAccount = paymentAccount; this.paymentAccount = paymentAccount;
} }
void useSavingsWalletForFunding() {
useSavingsWallet = true;
updateBalance();
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Getters // Getters
@ -275,10 +298,6 @@ class TakeOfferDataModel extends ActivatableDataModel {
return user.getAcceptedArbitrators().size() > 0; return user.getAcceptedArbitrators().size() > 0;
} }
boolean isFeeFromFundingTxSufficient() {
return feeFromFundingTxProperty.get().compareTo(FeePolicy.getMinRequiredFeeForFundingTx()) >= 0;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Bindings, listeners // Bindings, listeners
@ -311,7 +330,7 @@ class TakeOfferDataModel extends ActivatableDataModel {
!amountAsCoin.get().isZero()) { !amountAsCoin.get().isZero()) {
volumeAsFiat.set(new ExchangeRate(offer.getPrice()).coinToFiat(amountAsCoin.get())); 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())); totalToPayAsCoin.set(takerFeeAsCoin.add(networkFeeAsCoin).add(securityDepositAsCoin).add(amountAsCoin.get()));
} }
private void updateBalance(@NotNull Coin balance) { void updateBalance() {
isWalletFunded.set(totalToPayAsCoin.get() != null && balance.compareTo(totalToPayAsCoin.get()) >= 0); Coin tradeWalletBalance = walletService.getBalanceForAddress(addressEntry.getAddress());
if (useSavingsWallet) {
Coin savingWalletBalance = walletService.getSavingWalletBalance();
totalAvailableBalance = savingWalletBalance.add(tradeWalletBalance);
if (isWalletFunded.get()) if (totalAvailableBalance.compareTo(totalToPayAsCoin.get()) > 0)
walletService.removeBalanceListener(balanceListener); 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() { boolean isMinAmountLessOrEqualAmount() {

View file

@ -60,15 +60,16 @@ import javafx.stage.Window;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import net.glxn.qrgen.QRCode; import net.glxn.qrgen.QRCode;
import net.glxn.qrgen.image.ImageType; import net.glxn.qrgen.image.ImageType;
import org.bitcoinj.core.Coin;
import org.bitcoinj.uri.BitcoinURI; import org.bitcoinj.uri.BitcoinURI;
import org.controlsfx.control.PopOver; import org.controlsfx.control.PopOver;
import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription; import org.fxmisc.easybind.Subscription;
import org.fxmisc.easybind.monadic.MonadicBinding;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import javax.inject.Inject; import javax.inject.Inject;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.net.URI;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static io.bitsquare.gui.util.FormBuilder.*; import static io.bitsquare.gui.util.FormBuilder.*;
@ -89,7 +90,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
private BalanceTextField balanceTextField; private BalanceTextField balanceTextField;
private ProgressIndicator spinner, offerAvailabilitySpinner; private ProgressIndicator spinner, offerAvailabilitySpinner;
private TitledGroupBg payFundsPane; private TitledGroupBg payFundsPane;
private Button nextButton, takeOfferButton, cancelButton1, cancelButton2; private Button nextButton, cancelButton1, cancelButton2, fundFromSavingsWalletButton, fundFromExternalWalletButton, takeOfferButton;
private InputTextField amountTextField; private InputTextField amountTextField;
private TextField paymentMethodTextField, currencyTextField, priceTextField, volumeTextField, amountRangeTextField; private TextField paymentMethodTextField, currencyTextField, priceTextField, volumeTextField, amountRangeTextField;
private Label directionLabel, amountDescriptionLabel, addressLabel, balanceLabel, totalToPayLabel, totalToPayInfoIconLabel, private Label directionLabel, amountDescriptionLabel, addressLabel, balanceLabel, totalToPayLabel, totalToPayInfoIconLabel,
@ -109,11 +110,14 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
private Subscription showWarningInvalidBtcDecimalPlacesSubscription; private Subscription showWarningInvalidBtcDecimalPlacesSubscription;
private Subscription showTransactionPublishedScreenSubscription; private Subscription showTransactionPublishedScreenSubscription;
private SimpleBooleanProperty errorPopupDisplayed; private SimpleBooleanProperty errorPopupDisplayed;
private ChangeListener<Coin> feeFromFundingTxListener;
private boolean offerDetailsWindowDisplayed; private boolean offerDetailsWindowDisplayed;
private Notification walletFundedNotification; private Notification walletFundedNotification;
private Subscription isWalletFundedSubscription;
private ImageView qrCodeImageView; private ImageView qrCodeImageView;
private HBox fundingHBox;
private Subscription balanceSubscription;
private Subscription noSufficientFeeSubscription;
private MonadicBinding<Boolean> noSufficientFeeBinding;
private Subscription totalToPaySubscription;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -155,12 +159,19 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
amountTextField.textProperty().bindBidirectional(model.amount); amountTextField.textProperty().bindBidirectional(model.amount);
volumeTextField.textProperty().bindBidirectional(model.volume); volumeTextField.textProperty().bindBidirectional(model.volume);
totalToPayTextField.textProperty().bind(model.totalToPay); totalToPayTextField.textProperty().bind(model.totalToPay);
addressTextField.amountAsCoinProperty().bind(model.totalToPayAsCoin); addressTextField.amountAsCoinProperty().bind(model.dataModel.missingCoin);
amountTextField.validationResultProperty().bind(model.amountValidationResult); amountTextField.validationResultProperty().bind(model.amountValidationResult);
takeOfferButton.disableProperty().bind(model.isTakeOfferButtonDisabled); takeOfferButton.disableProperty().bind(model.isTakeOfferButtonDisabled);
takeOfferButton.visibleProperty().bind(model.dataModel.isWalletFunded.and(model.showPayFundsScreenDisplayed));
takeOfferButton.managedProperty().bind(model.dataModel.isWalletFunded.and(model.showPayFundsScreenDisplayed));
fundingHBox.visibleProperty().bind(model.dataModel.isWalletFunded.not().and(model.showPayFundsScreenDisplayed));
fundingHBox.managedProperty().bind(model.dataModel.isWalletFunded.not().and(model.showPayFundsScreenDisplayed));
spinner.visibleProperty().bind(model.isSpinnerVisible); spinner.visibleProperty().bind(model.isSpinnerVisible);
spinner.managedProperty().bind(model.isSpinnerVisible);
spinnerInfoLabel.visibleProperty().bind(model.isSpinnerVisible); spinnerInfoLabel.visibleProperty().bind(model.isSpinnerVisible);
spinnerInfoLabel.managedProperty().bind(model.isSpinnerVisible);
spinnerInfoLabel.textProperty().bind(model.spinnerInfoText); spinnerInfoLabel.textProperty().bind(model.spinnerInfoText);
priceCurrencyLabel.textProperty().bind(createStringBinding(() -> priceCurrencyLabel.textProperty().bind(createStringBinding(() ->
@ -256,14 +267,15 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
paymentAccountsComboBox.getSelectionModel().select(0); paymentAccountsComboBox.getSelectionModel().select(0);
} }
feeFromFundingTxListener = (observable, oldValue, newValue) -> { noSufficientFeeBinding = EasyBind.combine(model.dataModel.isWalletFunded, model.dataModel.isMainNet, model.dataModel.isFeeFromFundingTxSufficient,
log.debug("feeFromFundingTxListener " + newValue); (isWalletFunded, isMainNet, isFeeSufficient) -> isWalletFunded && isMainNet && !isFeeSufficient);
if (!model.dataModel.isFeeFromFundingTxSufficient()) { noSufficientFeeSubscription = noSufficientFeeBinding.subscribe((observable, oldValue, newValue) -> {
if (newValue)
new Popup().warning("The mining fee from your funding transaction is not sufficiently high.\n\n" + 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 " + "You need to use at least a mining fee of " +
model.formatter.formatCoinWithCode(FeePolicy.getMinRequiredFeeForFundingTx()) + ".\n\n" + model.formatter.formatCoinWithCode(FeePolicy.getMinRequiredFeeForFundingTx()) + ".\n\n" +
"The fee used in your funding transaction was only " + "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 " + "The trade transactions might take too much time to be included in " +
"a block if the fee is too low.\n" + "a block if the fee is too low.\n" +
"Please check at your external wallet that you set the required fee and " + "Please check at your external wallet that you set the required fee and " +
@ -275,15 +287,16 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class); navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class);
}) })
.show(); .show();
} });
};
model.dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener);
if (offerAvailabilitySpinner != null && offerAvailabilitySpinner.isVisible()) if (offerAvailabilitySpinner != null && offerAvailabilitySpinner.isVisible())
offerAvailabilitySpinner.setProgress(-1); offerAvailabilitySpinner.setProgress(-1);
if (spinner != null && spinner.isVisible()) if (spinner != null && spinner.isVisible())
spinner.setProgress(-1); spinner.setProgress(-1);
balanceSubscription = EasyBind.subscribe(model.dataModel.balance, newValue -> balanceTextField.setBalance(newValue));
totalToPaySubscription = EasyBind.subscribe(model.dataModel.totalToPayAsCoin, newValue -> balanceTextField.setTargetAmount(newValue));
} }
@Override @Override
@ -298,16 +311,23 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
totalToPayTextField.textProperty().unbind(); totalToPayTextField.textProperty().unbind();
addressTextField.amountAsCoinProperty().unbind(); addressTextField.amountAsCoinProperty().unbind();
amountTextField.validationResultProperty().unbind(); amountTextField.validationResultProperty().unbind();
takeOfferButton.disableProperty().unbind();
spinner.visibleProperty().unbind();
spinnerInfoLabel.visibleProperty().unbind();
spinnerInfoLabel.textProperty().unbind();
priceCurrencyLabel.textProperty().unbind(); priceCurrencyLabel.textProperty().unbind();
volumeCurrencyLabel.textProperty().unbind(); volumeCurrencyLabel.textProperty().unbind();
amountRangeBtcLabel.textProperty().unbind(); amountRangeBtcLabel.textProperty().unbind();
priceDescriptionLabel.textProperty().unbind(); priceDescriptionLabel.textProperty().unbind();
volumeDescriptionLabel.textProperty().unbind(); volumeDescriptionLabel.textProperty().unbind();
fundingHBox.visibleProperty().unbind();
fundingHBox.managedProperty().unbind();
takeOfferButton.visibleProperty().unbind();
takeOfferButton.managedProperty().unbind();
takeOfferButton.disableProperty().unbind();
spinner.visibleProperty().unbind();
spinner.managedProperty().unbind();
spinnerInfoLabel.visibleProperty().unbind();
spinnerInfoLabel.managedProperty().unbind();
spinnerInfoLabel.textProperty().unbind();
offerWarningSubscription.unsubscribe(); offerWarningSubscription.unsubscribe();
errorMessageSubscription.unsubscribe(); errorMessageSubscription.unsubscribe();
isOfferAvailableSubscription.unsubscribe(); isOfferAvailableSubscription.unsubscribe();
@ -315,7 +335,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
showWarningInvalidBtcDecimalPlacesSubscription.unsubscribe(); showWarningInvalidBtcDecimalPlacesSubscription.unsubscribe();
showTransactionPublishedScreenSubscription.unsubscribe(); showTransactionPublishedScreenSubscription.unsubscribe();
model.dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); noSufficientFeeSubscription.unsubscribe();
if (balanceTextField != null) if (balanceTextField != null)
balanceTextField.cleanup(); balanceTextField.cleanup();
@ -325,8 +345,8 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
if (spinner != null) if (spinner != null)
spinner.setProgress(0); spinner.setProgress(0);
if (isWalletFundedSubscription != null) balanceSubscription.unsubscribe();
isWalletFundedSubscription.unsubscribe(); totalToPaySubscription.unsubscribe();
} }
@ -382,6 +402,15 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
// called form parent as the view does not get notified when the tab is closed // called form parent as the view does not get notified when the tab is closed
public void onClose() { public void onClose() {
if (model.dataModel.balance.get().isPositive() && !model.takeOfferCompleted.get()) {
model.dataModel.swapTradeToSavings();
new Popup().information("You have already funds paid in.\n" +
"In the \"Funds/Available for withdrawal\" section you can withdraw those funds.")
.actionButtonText("Go to \"Funds/Available for withdrawal\"")
.onAction(() -> 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 // TODO need other implementation as it is displayed also if there are old funds in the wallet
/* /*
if (model.dataModel.isWalletFunded.get()) if (model.dataModel.isWalletFunded.get())
@ -461,7 +490,6 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
offerAvailabilitySpinnerLabel.setVisible(false); offerAvailabilitySpinnerLabel.setVisible(false);
cancelButton1.setVisible(false); cancelButton1.setVisible(false);
cancelButton1.setOnAction(null); cancelButton1.setOnAction(null);
takeOfferButton.setVisible(true);
cancelButton2.setVisible(true); cancelButton2.setVisible(true);
spinner.setProgress(-1); spinner.setProgress(-1);
@ -487,19 +515,6 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
.autoClose(); .autoClose();
walletFundedNotification.show(); walletFundedNotification.show();
} }
} else {
isWalletFundedSubscription = EasyBind.subscribe(model.dataModel.isWalletFunded, isFunded -> {
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 final byte[] imageBytes = QRCode
@ -689,9 +704,41 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
balanceTextField = balanceTuple.second; balanceTextField = balanceTuple.second;
balanceTextField.setVisible(false); balanceTextField.setVisible(false);
Tuple3<Button, ProgressIndicator, Label> takeOfferTuple = addButtonWithStatusAfterGroup(gridPane, ++gridRow, ""); fundingHBox = new HBox();
takeOfferButton = takeOfferTuple.first; 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.setVisible(false);
takeOfferButton.setManaged(false);
takeOfferButton.setMinHeight(40); takeOfferButton.setMinHeight(40);
takeOfferButton.setPadding(new Insets(0, 20, 0, 20)); takeOfferButton.setPadding(new Insets(0, 20, 0, 20));
takeOfferButton.setOnAction(e -> { takeOfferButton.setOnAction(e -> {
@ -699,9 +746,6 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
balanceTextField.cleanup(); balanceTextField.cleanup();
}); });
spinner = takeOfferTuple.second;
spinnerInfoLabel = takeOfferTuple.third;
cancelButton2 = addButton(gridPane, ++gridRow, BSResources.get("shared.cancel")); cancelButton2 = addButton(gridPane, ++gridRow, BSResources.get("shared.cancel"));
cancelButton2.setOnAction(e -> { cancelButton2.setOnAction(e -> {
if (model.dataModel.isWalletFunded.get()) if (model.dataModel.isWalletFunded.get())
@ -716,12 +760,11 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
}); });
cancelButton2.setDefaultButton(false); cancelButton2.setDefaultButton(false);
cancelButton2.setVisible(false); cancelButton2.setVisible(false);
cancelButton2.setId("cancel-button");
} }
@NotNull @NotNull
private String getBitcoinURI() { private String getBitcoinURI() {
return model.getAddressAsString() != null ? BitcoinURI.convertToBitcoinURI(model.getAddressAsString(), model.totalToPayAsCoin.get(), return model.getAddressAsString() != null ? BitcoinURI.convertToBitcoinURI(model.getAddressAsString(), model.dataModel.missingCoin.get(),
model.getPaymentLabel(), null) : ""; model.getPaymentLabel(), null) : "";
} }

View file

@ -18,9 +18,13 @@
package io.bitsquare.gui.main.offer.takeoffer; package io.bitsquare.gui.main.offer.takeoffer;
import io.bitsquare.arbitration.Arbitrator; import io.bitsquare.arbitration.Arbitrator;
import io.bitsquare.btc.FeePolicy; import io.bitsquare.gui.Navigation;
import io.bitsquare.gui.common.model.ActivatableWithDataModel; import io.bitsquare.gui.common.model.ActivatableWithDataModel;
import io.bitsquare.gui.common.model.ViewModel; 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.overlays.popups.Popup;
import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.gui.util.validation.BtcValidator; import io.bitsquare.gui.util.validation.BtcValidator;
import io.bitsquare.gui.util.validation.InputValidator; import io.bitsquare.gui.util.validation.InputValidator;
@ -38,6 +42,8 @@ import javafx.beans.value.ChangeListener;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import org.bitcoinj.core.Address; import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.List; import java.util.List;
@ -46,8 +52,10 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static javafx.beans.binding.Bindings.createStringBinding; import static javafx.beans.binding.Bindings.createStringBinding;
class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> implements ViewModel { class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> implements ViewModel {
final TakeOfferDataModel dataModel;
private final BtcValidator btcValidator; private final BtcValidator btcValidator;
private final P2PService p2PService; private final P2PService p2PService;
private final Navigation navigation;
final BSFormatter formatter; final BSFormatter formatter;
private String amountRange; private String amountRange;
@ -75,11 +83,12 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
final BooleanProperty isSpinnerVisible = new SimpleBooleanProperty(); final BooleanProperty isSpinnerVisible = new SimpleBooleanProperty();
final BooleanProperty showWarningInvalidBtcDecimalPlaces = new SimpleBooleanProperty(); final BooleanProperty showWarningInvalidBtcDecimalPlaces = new SimpleBooleanProperty();
final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty();
final BooleanProperty takeOfferCompleted = new SimpleBooleanProperty();
final BooleanProperty showPayFundsScreenDisplayed = new SimpleBooleanProperty();
final ObjectProperty<InputValidator.ValidationResult> amountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty<InputValidator.ValidationResult> amountValidationResult = new SimpleObjectProperty<>();
// Those are needed for the addressTextField // Those are needed for the addressTextField
final ObjectProperty<Coin> totalToPayAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Address> address = new SimpleObjectProperty<>(); final ObjectProperty<Address> address = new SimpleObjectProperty<>();
private ChangeListener<String> amountListener; private ChangeListener<String> amountListener;
@ -90,9 +99,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
private ChangeListener<Offer.State> offerStateListener; private ChangeListener<Offer.State> offerStateListener;
private ChangeListener<String> offerErrorListener; private ChangeListener<String> offerErrorListener;
private ConnectionListener connectionListener; private ConnectionListener connectionListener;
private ChangeListener<Coin> feeFromFundingTxListener; private Subscription isFeeSufficientSubscription;
private Runnable takeOfferSucceededHandler; private Runnable takeOfferSucceededHandler;
private boolean showPayFundsScreenDisplayed;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -101,11 +109,13 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
@Inject @Inject
public TakeOfferViewModel(TakeOfferDataModel dataModel, BtcValidator btcValidator, P2PService p2PService, public TakeOfferViewModel(TakeOfferDataModel dataModel, BtcValidator btcValidator, P2PService p2PService,
BSFormatter formatter) { Navigation navigation, BSFormatter formatter) {
super(dataModel); super(dataModel);
this.dataModel = dataModel;
this.btcValidator = btcValidator; this.btcValidator = btcValidator;
this.p2PService = p2PService; this.p2PService = p2PService;
this.navigation = navigation;
this.formatter = formatter; this.formatter = formatter;
createListeners(); createListeners();
@ -190,6 +200,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
trade.errorMessageProperty().addListener(tradeErrorListener); trade.errorMessageProperty().addListener(tradeErrorListener);
applyTradeErrorMessage(trade.errorMessageProperty().get()); applyTradeErrorMessage(trade.errorMessageProperty().get());
updateButtonDisableState(); updateButtonDisableState();
takeOfferCompleted.set(true);
}); });
} }
@ -200,10 +211,30 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
} }
public void onShowPayFundsScreen() { public void onShowPayFundsScreen() {
showPayFundsScreenDisplayed = true; showPayFundsScreenDisplayed.set(true);
updateSpinnerInfo(); 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 // Handle focus
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -286,10 +317,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
break; break;
} }
if (offerWarning.get() != null) { updateSpinnerInfo();
isSpinnerVisible.set(false);
spinnerInfoText.set("");
}
updateButtonDisableState(); updateButtonDisableState();
} }
@ -328,8 +356,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
} }
this.errorMessage.set(errorMessage + appendMsg); this.errorMessage.set(errorMessage + appendMsg);
isSpinnerVisible.set(false); updateSpinnerInfo();
spinnerInfoText.set("");
if (takeOfferSucceededHandler != null) if (takeOfferSucceededHandler != null)
takeOfferSucceededHandler.run(); takeOfferSucceededHandler.run();
@ -349,9 +376,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
if (takeOfferSucceededHandler != null) if (takeOfferSucceededHandler != null)
takeOfferSucceededHandler.run(); takeOfferSucceededHandler.run();
isSpinnerVisible.set(false);
spinnerInfoText.set("");
showTransactionPublishedScreen.set(true); showTransactionPublishedScreen.set(true);
updateSpinnerInfo();
} else { } else {
log.error("trade.getDepositTx() == null. That must not happen"); log.error("trade.getDepositTx() == null. That must not happen");
} }
@ -364,10 +390,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
&& !dataModel.isAmountLargerThanOfferAmount() && !dataModel.isAmountLargerThanOfferAmount()
&& isOfferAvailable.get(); && isOfferAvailable.get();
isNextButtonDisabled.set(!inputDataValid); isNextButtonDisabled.set(!inputDataValid);
isTakeOfferButtonDisabled.set(!(inputDataValid boolean notSufficientFees = dataModel.isWalletFunded.get() && dataModel.isMainNet.get() && !dataModel.isFeeFromFundingTxSufficient.get();
&& dataModel.isWalletFunded.get() isTakeOfferButtonDisabled.set(takeOfferRequested || !inputDataValid || notSufficientFees);
&& !takeOfferRequested
&& dataModel.isFeeFromFundingTxSufficient()));
} }
@ -385,7 +409,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
volumeDescriptionLabel.set(BSResources.get("createOffer.amountPriceBox.sell.volumeDescription", dataModel.getCurrencyCode())); volumeDescriptionLabel.set(BSResources.get("createOffer.amountPriceBox.sell.volumeDescription", dataModel.getCurrencyCode()));
} }
totalToPay.bind(createStringBinding(() -> formatter.formatCoinWithCode(dataModel.totalToPayAsCoin.get()), dataModel.totalToPayAsCoin)); totalToPay.bind(createStringBinding(() -> formatter.formatCoinWithCode(dataModel.totalToPayAsCoin.get()), dataModel.totalToPayAsCoin));
totalToPayAsCoin.bind(dataModel.totalToPayAsCoin);
btcCode.bind(dataModel.btcCode); btcCode.bind(dataModel.btcCode);
} }
@ -394,7 +417,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
volumeDescriptionLabel.unbind(); volumeDescriptionLabel.unbind();
volume.unbind(); volume.unbind();
totalToPay.unbind(); totalToPay.unbind();
totalToPayAsCoin.unbind();
btcCode.unbind(); btcCode.unbind();
} }
@ -410,16 +432,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
amountAsCoinListener = (ov, oldValue, newValue) -> amount.set(formatter.formatCoin(newValue)); amountAsCoinListener = (ov, oldValue, newValue) -> amount.set(formatter.formatCoin(newValue));
isWalletFundedListener = (ov, oldValue, newValue) -> { isWalletFundedListener = (ov, oldValue, newValue) -> {
updateButtonDisableState(); 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); tradeStateListener = (ov, oldValue, newValue) -> applyTradeState(newValue);
tradeErrorListener = (ov, oldValue, newValue) -> applyTradeErrorMessage(newValue); tradeErrorListener = (ov, oldValue, newValue) -> applyTradeErrorMessage(newValue);
offerStateListener = (ov, oldValue, newValue) -> applyOfferState(newValue); offerStateListener = (ov, oldValue, newValue) -> applyOfferState(newValue);
@ -432,8 +446,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
"He might have gone offline or has closed the connection to you because of too " + "He might have gone offline or has closed the connection to you because of too " +
"many open connections.\n\n" + "many open connections.\n\n" +
"If you can still see his offer in the offerbook you can try to take the offer again."); "If you can still see his offer in the offerbook you can try to take the offer again.");
isSpinnerVisible.set(false); updateSpinnerInfo();
spinnerInfoText.set("");
} }
} }
@ -448,15 +461,23 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
} }
private void updateSpinnerInfo() { private void updateSpinnerInfo() {
if (dataModel.isWalletFunded.get() || !showPayFundsScreenDisplayed) { if (!showPayFundsScreenDisplayed.get() ||
isSpinnerVisible.set(false); offerWarning.get() != null ||
errorMessage.get() != null ||
showTransactionPublishedScreen.get()) {
spinnerInfoText.set(""); spinnerInfoText.set("");
} else if (showPayFundsScreenDisplayed) { } else if (dataModel.isWalletFunded.get()) {
spinnerInfoText.set("Waiting for receiving funds..."); if (dataModel.isFeeFromFundingTxSufficient.get()) {
isSpinnerVisible.set(true); 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() { private void addListeners() {
// Bidirectional bindings are used for all input fields: amount, price, volume and minAmount // Bidirectional bindings are used for all input fields: amount, price, volume and minAmount
@ -468,7 +489,10 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
dataModel.isWalletFunded.addListener(isWalletFundedListener); dataModel.isWalletFunded.addListener(isWalletFundedListener);
p2PService.getNetworkNode().addConnectionListener(connectionListener); p2PService.getNetworkNode().addConnectionListener(connectionListener);
dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener); isFeeSufficientSubscription = EasyBind.subscribe(dataModel.isFeeFromFundingTxSufficient, newValue -> {
updateButtonDisableState();
updateSpinnerInfo();
});
} }
private void removeListeners() { private void removeListeners() {
@ -488,7 +512,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
trade.errorMessageProperty().removeListener(tradeErrorListener); trade.errorMessageProperty().removeListener(tradeErrorListener);
} }
p2PService.getNetworkNode().removeConnectionListener(connectionListener); p2PService.getNetworkNode().removeConnectionListener(connectionListener);
dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener); isFeeSufficientSubscription.unsubscribe();
} }