From 56f4caf7bcec20491cf5720722d3876aa0f52226 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Sun, 10 Jun 2018 15:22:17 +0200 Subject: [PATCH] Add basic UI for bond lockup --- .../java/bisq/desktop/main/dao/DaoView.java | 15 +- .../desktop/main/dao/bonding/BondingView.fxml | 31 +++ .../desktop/main/dao/bonding/BondingView.java | 120 +++++++++ .../main/dao/bonding/Lock/LockupBSQView.fxml | 33 +++ .../main/dao/bonding/Lock/LockupBSQView.java | 230 ++++++++++++++++++ 5 files changed, 427 insertions(+), 2 deletions(-) create mode 100644 src/main/java/bisq/desktop/main/dao/bonding/BondingView.fxml create mode 100644 src/main/java/bisq/desktop/main/dao/bonding/BondingView.java create mode 100644 src/main/java/bisq/desktop/main/dao/bonding/Lock/LockupBSQView.fxml create mode 100644 src/main/java/bisq/desktop/main/dao/bonding/Lock/LockupBSQView.java diff --git a/src/main/java/bisq/desktop/main/dao/DaoView.java b/src/main/java/bisq/desktop/main/dao/DaoView.java index ac70e01937..ed9250ee1e 100644 --- a/src/main/java/bisq/desktop/main/dao/DaoView.java +++ b/src/main/java/bisq/desktop/main/dao/DaoView.java @@ -25,6 +25,7 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.common.view.View; import bisq.desktop.common.view.ViewLoader; import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.bonding.BondingView; import bisq.desktop.main.dao.proposal.ProposalView; import bisq.desktop.main.dao.voting.VotingView; import bisq.desktop.main.dao.wallet.BsqWalletView; @@ -48,7 +49,7 @@ import javafx.beans.value.ChangeListener; public class DaoView extends ActivatableViewAndModel { @FXML - Tab bsqWalletTab, proposalsTab, votingTab; + Tab bsqWalletTab, proposalsTab, votingTab, bondingTab; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; @@ -69,13 +70,15 @@ public class DaoView extends ActivatableViewAndModel { public void initialize() { proposalsTab = new Tab(Res.get("dao.tab.proposals")); votingTab = new Tab(Res.get("dao.tab.voting")); + bondingTab = new Tab(Res.get("dao.tab.bonding")); proposalsTab.setClosable(false); votingTab.setClosable(false); //TODO - root.getTabs().addAll(proposalsTab, votingTab); + root.getTabs().addAll(proposalsTab, votingTab, bondingTab); if (!BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq() || !DevEnv.isDaoPhase2Activated()) { votingTab.setDisable(true); + bondingTab.setDisable(true); proposalsTab.setDisable(true); } @@ -106,6 +109,9 @@ public class DaoView extends ActivatableViewAndModel { } else if (newValue == votingTab) { //noinspection unchecked navigation.navigateTo(MainView.class, DaoView.class, VotingView.class); + } else if (newValue == bondingTab) { + //noinspection unchecked + navigation.navigateTo(MainView.class, DaoView.class, BondingView.class); } }; } @@ -126,6 +132,9 @@ public class DaoView extends ActivatableViewAndModel { else if (selectedItem == votingTab) //noinspection unchecked navigation.navigateTo(MainView.class, DaoView.class, VotingView.class); + else if (selectedItem == bondingTab) + //noinspection unchecked + navigation.navigateTo(MainView.class, DaoView.class, BondingView.class); } } @@ -144,6 +153,8 @@ public class DaoView extends ActivatableViewAndModel { selectedTab = proposalsTab; } else if (view instanceof VotingView) { selectedTab = votingTab; + } else if (view instanceof BondingView) { + selectedTab = bondingTab; } selectedTab.setContent(view.getRoot()); diff --git a/src/main/java/bisq/desktop/main/dao/bonding/BondingView.fxml b/src/main/java/bisq/desktop/main/dao/bonding/BondingView.fxml new file mode 100644 index 0000000000..64f72f9e4f --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/BondingView.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/src/main/java/bisq/desktop/main/dao/bonding/BondingView.java b/src/main/java/bisq/desktop/main/dao/bonding/BondingView.java new file mode 100644 index 0000000000..45ce35b195 --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/BondingView.java @@ -0,0 +1,120 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.bonding; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableViewAndModel; +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.common.view.ViewPath; +import bisq.desktop.components.MenuItem; +import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.DaoView; +import bisq.desktop.main.dao.bonding.Lock.LockupBSQView; + +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.fxml.FXML; + +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; + +import java.util.Arrays; +import java.util.List; + +@FxmlView +public class BondingView extends ActivatableViewAndModel { + + private final ViewLoader viewLoader; + private final Navigation navigation; + + private MenuItem lockupBSQ; + private Navigation.Listener listener; + + @FXML + private VBox leftVBox; + @FXML + private AnchorPane content; + + private Class selectedViewClass; + + @Inject + private BondingView(CachingViewLoader viewLoader, Navigation navigation) { + this.viewLoader = viewLoader; + this.navigation = navigation; + } + + @Override + public void initialize() { + listener = viewPath -> { + if (viewPath.size() != 4 || viewPath.indexOf(bisq.desktop.main.dao.bonding.BondingView.class) != 2) + return; + + selectedViewClass = viewPath.tip(); + loadView(selectedViewClass); + }; + + ToggleGroup toggleGroup = new ToggleGroup(); + final List> baseNavPath = Arrays.asList(MainView.class, DaoView.class, bisq.desktop.main.dao.bonding.BondingView.class); + lockupBSQ = new MenuItem(navigation, toggleGroup, Res.get("dao.bonding.menuItem.lockupBSQ"), + LockupBSQView.class, AwesomeIcon.LIST_UL, baseNavPath); + leftVBox.getChildren().addAll(lockupBSQ); + } + + @Override + protected void activate() { + navigation.addListener(listener); + ViewPath viewPath = navigation.getCurrentPath(); + if (viewPath.size() == 3 && viewPath.indexOf(BondingView.class) == 2 || + viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) { + if (selectedViewClass == null) + selectedViewClass = LockupBSQView.class; + + loadView(selectedViewClass); + + } else if (viewPath.size() == 4 && viewPath.indexOf(BondingView.class) == 2) { + selectedViewClass = viewPath.get(3); + loadView(selectedViewClass); + } + } + + @Override + protected void deactivate() { + navigation.removeListener(listener); + + lockupBSQ.deactivate(); + } + + private void loadView(Class viewClass) { + View view = viewLoader.load(viewClass); + content.getChildren().setAll(view.getRoot()); + + if (view instanceof LockupBSQView) lockupBSQ.setSelected(true); + } + + public Class getSelectedViewClass() { + return selectedViewClass; + } +} diff --git a/src/main/java/bisq/desktop/main/dao/bonding/Lock/LockupBSQView.fxml b/src/main/java/bisq/desktop/main/dao/bonding/Lock/LockupBSQView.fxml new file mode 100644 index 0000000000..d36a497c35 --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/Lock/LockupBSQView.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + diff --git a/src/main/java/bisq/desktop/main/dao/bonding/Lock/LockupBSQView.java b/src/main/java/bisq/desktop/main/dao/bonding/Lock/LockupBSQView.java new file mode 100644 index 0000000000..c359d131a2 --- /dev/null +++ b/src/main/java/bisq/desktop/main/dao/bonding/Lock/LockupBSQView.java @@ -0,0 +1,230 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.dao.bonding.Lock; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.wallet.BsqBalanceUtil; +import bisq.desktop.main.funds.FundsView; +import bisq.desktop.main.funds.deposit.DepositView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.BSFormatter; +import bisq.desktop.util.BsqFormatter; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BsqAddressValidator; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.btc.Restrictions; +import bisq.core.btc.wallet.BsqBalanceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TxBroadcastException; +import bisq.core.btc.wallet.TxBroadcastTimeoutException; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.TxMalleabilityException; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.btc.wallet.WalletsSetup; +import bisq.core.locale.Res; +import bisq.core.util.CoinUtil; + +import bisq.network.p2p.P2PService; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import javafx.scene.control.Button; +import javafx.scene.layout.GridPane; + +import javafx.beans.value.ChangeListener; + +import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; +import static bisq.desktop.util.FormBuilder.addLabelInputTextField; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; + +@FxmlView +public class LockupBSQView extends ActivatableView implements BsqBalanceListener { + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + private final WalletsManager walletsManager; + private final WalletsSetup walletsSetup; + private final P2PService p2PService; + private final BsqFormatter bsqFormatter; + private final BSFormatter btcFormatter; + private final Navigation navigation; + private final BsqBalanceUtil bsqBalanceUtil; + private final BsqValidator bsqValidator; + + private int gridRow = 0; + private InputTextField amountInputTextField; + private Button lockupButton; + private ChangeListener focusOutListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private LockupBSQView(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + WalletsManager walletsManager, + WalletsSetup walletsSetup, + P2PService p2PService, + BsqFormatter bsqFormatter, + BSFormatter btcFormatter, + Navigation navigation, + BsqBalanceUtil bsqBalanceUtil, + BsqValidator bsqValidator, + BsqAddressValidator bsqAddressValidator) { + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + this.walletsManager = walletsManager; + this.walletsSetup = walletsSetup; + this.p2PService = p2PService; + this.bsqFormatter = bsqFormatter; + this.btcFormatter = btcFormatter; + this.navigation = navigation; + this.bsqBalanceUtil = bsqBalanceUtil; + this.bsqValidator = bsqValidator; + } + + @Override + public void initialize() { + // TODO: Show balance locked up in bonds + gridRow = bsqBalanceUtil.addGroup(root, gridRow); + + addTitledGroupBg(root, ++gridRow, 3, Res.get("dao.bonding.lock.lockBSQ"), Layout.GROUP_DISTANCE); + + amountInputTextField = addLabelInputTextField(root, gridRow, Res.get("dao.bonding.lock.amount"), + Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; + amountInputTextField.setPromptText(Res.get("dao.bonding.lock.setAmount", Restrictions.getMinNonDustOutput().value)); + amountInputTextField.setValidator(bsqValidator); + + focusOutListener = (observable, oldValue, newValue) -> { + if (!newValue) + onUpdateBalances(bsqWalletService.getAvailableBalance(), bsqWalletService.getPendingBalance(), + bsqWalletService.getLockedForVotingBalance(), bsqWalletService.getLockedInBondsBalance()); + }; + + lockupButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.bonding.lock.lockupButton")); + + lockupButton.setOnAction((event) -> { + if (GUIUtil.isReadyForTxBroadcast(p2PService, walletsSetup)) { + Address lockupAddress = bsqWalletService.getUnusedAddress(); + Coin lockupAmount = bsqFormatter.parseToCoin(amountInputTextField.getText()); + try { + // TODO: Implement lockup bond function (this is just sending BSQ) + Transaction preparedSendTx = bsqWalletService.getPreparedSendTx(lockupAddress.toString(), lockupAmount); + Transaction txWithBtcFee = btcWalletService.completePreparedSendBsqTx(preparedSendTx, true); + Transaction signedTx = bsqWalletService.signTx(txWithBtcFee); + Coin miningFee = signedTx.getFee(); + int txSize = signedTx.bitcoinSerialize().length; + new Popup<>().headLine(Res.get("dao.bonding.lock.sendFunds.headline")) + .confirmation(Res.get("dao.bonding.lock.sendFunds.details", + bsqFormatter.formatCoinWithCode(lockupAmount), + bsqFormatter.getBsqAddressStringFromAddress(lockupAddress), + btcFormatter.formatCoinWithCode(miningFee), + CoinUtil.getFeePerByte(miningFee, txSize), + txSize / 1000d, + bsqFormatter.formatCoinWithCode(lockupAmount))) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> { + walletsManager.publishAndCommitBsqTx(txWithBtcFee, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + log.debug("Successfully sent tx with id " + txWithBtcFee.getHashAsString()); + } + + @Override + public void onTimeout(TxBroadcastTimeoutException exception) { + //TODO handle + new Popup<>().warning(exception.toString()); + } + + @Override + public void onTxMalleability(TxMalleabilityException exception) { + //TODO handle + new Popup<>().warning(exception.toString()); + } + + @Override + public void onFailure(TxBroadcastException exception) { + //TODO handle + new Popup<>().warning(exception.toString()); + } + }); + + amountInputTextField.setText(""); + }) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } catch (Throwable t) { + if (t instanceof InsufficientMoneyException) { + final Coin missingCoin = ((InsufficientMoneyException) t).missing; + final String missing = missingCoin != null ? missingCoin.toFriendlyString() : "null"; + //noinspection unchecked + new Popup<>().warning(Res.get("popup.warning.insufficientBtcFundsForBsqTx", missing)) + .actionButtonTextWithGoTo("navigation.funds.depositFunds") + .onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class)) + .show(); + } else { + log.error(t.toString()); + t.printStackTrace(); + new Popup<>().warning(t.getMessage()).show(); + } + } + } else { + GUIUtil.showNotReadyForTxBroadcastPopups(p2PService, walletsSetup); + } + }); + } + + @Override + protected void activate() { + bsqBalanceUtil.activate(); + amountInputTextField.focusedProperty().addListener(focusOutListener); + bsqWalletService.addBsqBalanceListener(this); + onUpdateBalances(bsqWalletService.getAvailableBalance(), bsqWalletService.getPendingBalance(), + bsqWalletService.getLockedForVotingBalance(), bsqWalletService.getLockedInBondsBalance()); + } + + @Override + protected void deactivate() { + bsqBalanceUtil.deactivate(); + amountInputTextField.focusedProperty().removeListener(focusOutListener); + bsqWalletService.removeBsqBalanceListener(this); + } + + @Override + public void onUpdateBalances(Coin confirmedBalance, + Coin pendingBalance, + Coin lockedForVotingBalance, + Coin lockedInBondsBalance) { + bsqValidator.setAvailableBalance(confirmedBalance); + boolean isValid = bsqValidator.validate(amountInputTextField.getText()).isValid; + lockupButton.setDisable(!isValid); + } +}