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,
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();

View File

@ -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();

View File

@ -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);
}

View File

@ -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;
}
}

View File

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

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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);
}
}

View File

@ -177,6 +177,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
onPaymentAccountsComboBoxSelected();
balanceTextField.setBalance(model.dataModel.balance.get());
balanceTextField.setTargetAmount(model.dataModel.totalToPayAsCoin.get());
}
@Override
@ -268,9 +269,8 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
currencyComboBox.setMouseTransparent(true);
paymentAccountsComboBox.setMouseTransparent(true);
fundingHBox.visibleProperty().bind(model.dataModel.isWalletFunded.not());
fundingHBox.managedProperty().bind(model.dataModel.isWalletFunded.not());
balanceTextField.setTargetAmount(model.dataModel.totalToPayAsCoin.get());
if (!BitsquareApp.DEV_MODE) {
String key = "securityDepositInfo";
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);
// buttons
placeOfferButton.visibleProperty().bind(model.dataModel.isWalletFunded);
placeOfferButton.managedProperty().bind(model.dataModel.isWalletFunded);
fundingHBox.visibleProperty().bind(model.dataModel.isWalletFunded.not().and(model.showPayFundsScreenDisplayed));
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);
cancelButton2.disableProperty().bind(model.cancelButtonDisabled);

View File

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

View File

@ -29,6 +29,7 @@ import io.bitsquare.btc.listeners.BalanceListener;
import io.bitsquare.btc.pricefeed.PriceFeed;
import io.bitsquare.common.UserThread;
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.windows.WalletPasswordWindow;
import io.bitsquare.gui.util.BSFormatter;
@ -46,6 +47,7 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionConfidence;
import org.bitcoinj.utils.ExchangeRate;
import org.bitcoinj.utils.Fiat;
import org.jetbrains.annotations.NotNull;
@ -75,21 +77,27 @@ class TakeOfferDataModel extends ActivatableDataModel {
private final Coin takerFeeAsCoin;
private final Coin networkFeeAsCoin;
private final Coin securityDepositAsCoin;
Coin feeFromFundingTx = Coin.NEGATIVE_SATOSHI;
private Offer offer;
private AddressEntry addressEntry;
private AddressEntry addressEntry;
final StringProperty btcCode = new SimpleStringProperty();
final BooleanProperty useMBTC = 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<Fiat> volumeAsFiat = 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;
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<Coin> future = blockchainService.requestFee(tx.getHashAsString());
Futures.addCallback(future, new FutureCallback<Coin>() {
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() {

View File

@ -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<AnchorPane, TakeOffer
private BalanceTextField balanceTextField;
private ProgressIndicator spinner, offerAvailabilitySpinner;
private TitledGroupBg payFundsPane;
private Button nextButton, takeOfferButton, cancelButton1, cancelButton2;
private Button nextButton, cancelButton1, cancelButton2, fundFromSavingsWalletButton, fundFromExternalWalletButton, takeOfferButton;
private InputTextField amountTextField;
private TextField paymentMethodTextField, currencyTextField, priceTextField, volumeTextField, amountRangeTextField;
private Label directionLabel, amountDescriptionLabel, addressLabel, balanceLabel, totalToPayLabel, totalToPayInfoIconLabel,
@ -109,11 +110,14 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
private Subscription showWarningInvalidBtcDecimalPlacesSubscription;
private Subscription showTransactionPublishedScreenSubscription;
private SimpleBooleanProperty errorPopupDisplayed;
private ChangeListener<Coin> feeFromFundingTxListener;
private boolean offerDetailsWindowDisplayed;
private Notification walletFundedNotification;
private Subscription isWalletFundedSubscription;
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);
volumeTextField.textProperty().bindBidirectional(model.volume);
totalToPayTextField.textProperty().bind(model.totalToPay);
addressTextField.amountAsCoinProperty().bind(model.totalToPayAsCoin);
addressTextField.amountAsCoinProperty().bind(model.dataModel.missingCoin);
amountTextField.validationResultProperty().bind(model.amountValidationResult);
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.managedProperty().bind(model.isSpinnerVisible);
spinnerInfoLabel.visibleProperty().bind(model.isSpinnerVisible);
spinnerInfoLabel.managedProperty().bind(model.isSpinnerVisible);
spinnerInfoLabel.textProperty().bind(model.spinnerInfoText);
priceCurrencyLabel.textProperty().bind(createStringBinding(() ->
@ -256,14 +267,15 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
paymentAccountsComboBox.getSelectionModel().select(0);
}
feeFromFundingTxListener = (observable, oldValue, newValue) -> {
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<AnchorPane, TakeOffer
navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class);
})
.show();
}
};
model.dataModel.feeFromFundingTxProperty.addListener(feeFromFundingTxListener);
});
if (offerAvailabilitySpinner != null && offerAvailabilitySpinner.isVisible())
offerAvailabilitySpinner.setProgress(-1);
if (spinner != null && spinner.isVisible())
spinner.setProgress(-1);
balanceSubscription = EasyBind.subscribe(model.dataModel.balance, newValue -> balanceTextField.setBalance(newValue));
totalToPaySubscription = EasyBind.subscribe(model.dataModel.totalToPayAsCoin, newValue -> balanceTextField.setTargetAmount(newValue));
}
@Override
@ -298,16 +311,23 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
totalToPayTextField.textProperty().unbind();
addressTextField.amountAsCoinProperty().unbind();
amountTextField.validationResultProperty().unbind();
takeOfferButton.disableProperty().unbind();
spinner.visibleProperty().unbind();
spinnerInfoLabel.visibleProperty().unbind();
spinnerInfoLabel.textProperty().unbind();
priceCurrencyLabel.textProperty().unbind();
volumeCurrencyLabel.textProperty().unbind();
amountRangeBtcLabel.textProperty().unbind();
priceDescriptionLabel.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();
errorMessageSubscription.unsubscribe();
isOfferAvailableSubscription.unsubscribe();
@ -315,7 +335,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
showWarningInvalidBtcDecimalPlacesSubscription.unsubscribe();
showTransactionPublishedScreenSubscription.unsubscribe();
model.dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener);
noSufficientFeeSubscription.unsubscribe();
if (balanceTextField != null)
balanceTextField.cleanup();
@ -325,8 +345,8 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
if (spinner != null)
spinner.setProgress(0);
if (isWalletFundedSubscription != null)
isWalletFundedSubscription.unsubscribe();
balanceSubscription.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
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
/*
if (model.dataModel.isWalletFunded.get())
@ -461,7 +490,6 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
offerAvailabilitySpinnerLabel.setVisible(false);
cancelButton1.setVisible(false);
cancelButton1.setOnAction(null);
takeOfferButton.setVisible(true);
cancelButton2.setVisible(true);
spinner.setProgress(-1);
@ -487,19 +515,6 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
.autoClose();
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
@ -689,9 +704,41 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
balanceTextField = balanceTuple.second;
balanceTextField.setVisible(false);
Tuple3<Button, ProgressIndicator, Label> 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<AnchorPane, TakeOffer
balanceTextField.cleanup();
});
spinner = takeOfferTuple.second;
spinnerInfoLabel = takeOfferTuple.third;
cancelButton2 = addButton(gridPane, ++gridRow, BSResources.get("shared.cancel"));
cancelButton2.setOnAction(e -> {
if (model.dataModel.isWalletFunded.get())
@ -716,12 +760,11 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
});
cancelButton2.setDefaultButton(false);
cancelButton2.setVisible(false);
cancelButton2.setId("cancel-button");
}
@NotNull
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) : "";
}

View File

@ -18,9 +18,13 @@
package io.bitsquare.gui.main.offer.takeoffer;
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.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.validation.BtcValidator;
import io.bitsquare.gui.util.validation.InputValidator;
@ -38,6 +42,8 @@ import javafx.beans.value.ChangeListener;
import javafx.collections.ObservableList;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import javax.inject.Inject;
import java.util.List;
@ -46,8 +52,10 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static javafx.beans.binding.Bindings.createStringBinding;
class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> 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<TakeOfferDataModel> 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<InputValidator.ValidationResult> amountValidationResult = new SimpleObjectProperty<>();
// Those are needed for the addressTextField
final ObjectProperty<Coin> totalToPayAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Address> address = new SimpleObjectProperty<>();
private ChangeListener<String> amountListener;
@ -90,9 +99,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
private ChangeListener<Offer.State> offerStateListener;
private ChangeListener<String> offerErrorListener;
private ConnectionListener connectionListener;
private ChangeListener<Coin> feeFromFundingTxListener;
private Subscription isFeeSufficientSubscription;
private Runnable takeOfferSucceededHandler;
private boolean showPayFundsScreenDisplayed;
///////////////////////////////////////////////////////////////////////////////////////////
@ -101,11 +109,13 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> 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<TakeOfferDataModel> im
trade.errorMessageProperty().addListener(tradeErrorListener);
applyTradeErrorMessage(trade.errorMessageProperty().get());
updateButtonDisableState();
takeOfferCompleted.set(true);
});
}
@ -200,10 +211,30 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> 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<TakeOfferDataModel> im
break;
}
if (offerWarning.get() != null) {
isSpinnerVisible.set(false);
spinnerInfoText.set("");
}
updateSpinnerInfo();
updateButtonDisableState();
}
@ -328,8 +356,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> 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<TakeOfferDataModel> 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<TakeOfferDataModel> 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<TakeOfferDataModel> 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<TakeOfferDataModel> im
volumeDescriptionLabel.unbind();
volume.unbind();
totalToPay.unbind();
totalToPayAsCoin.unbind();
btcCode.unbind();
}
@ -410,16 +432,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> 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<TakeOfferDataModel> 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<TakeOfferDataModel> 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<TakeOfferDataModel> 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<TakeOfferDataModel> im
trade.errorMessageProperty().removeListener(tradeErrorListener);
}
p2PService.getNetworkNode().removeConnectionListener(connectionListener);
dataModel.feeFromFundingTxProperty.removeListener(feeFromFundingTxListener);
isFeeSufficientSubscription.unsubscribe();
}