/* * This file is part of Bitsquare. * * Bitsquare is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or (at * your option) any later version. * * Bitsquare is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public * License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Bitsquare. If not, see <http://www.gnu.org/licenses/>. */ package io.bitsquare.gui.main.funds.deposit; import de.jensd.fx.fontawesome.AwesomeIcon; import io.bitsquare.app.DevFlags; import io.bitsquare.btc.AddressEntry; import io.bitsquare.btc.Restrictions; import io.bitsquare.btc.WalletService; import io.bitsquare.btc.listeners.BalanceListener; import io.bitsquare.common.UserThread; import io.bitsquare.common.util.Tuple2; import io.bitsquare.gui.common.view.ActivatableView; import io.bitsquare.gui.common.view.FxmlView; import io.bitsquare.gui.components.AddressTextField; import io.bitsquare.gui.components.HyperlinkWithIcon; import io.bitsquare.gui.components.InputTextField; import io.bitsquare.gui.components.TitledGroupBg; import io.bitsquare.gui.main.overlays.popups.Popup; import io.bitsquare.gui.main.overlays.windows.QRCodeWindow; import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.GUIUtil; import io.bitsquare.gui.util.Layout; import io.bitsquare.user.Preferences; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; import javafx.fxml.FXML; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.VBox; import javafx.util.Callback; import net.glxn.qrgen.QRCode; import net.glxn.qrgen.image.ImageType; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.bitcoinj.uri.BitcoinURI; import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.Subscription; import org.jetbrains.annotations.NotNull; import javax.inject.Inject; import java.io.ByteArrayInputStream; import java.util.concurrent.TimeUnit; import static io.bitsquare.gui.util.FormBuilder.*; @FxmlView public class DepositView extends ActivatableView<VBox, Void> { @FXML GridPane gridPane; @FXML TableView<DepositListItem> tableView; @FXML TableColumn<DepositListItem, DepositListItem> selectColumn, addressColumn, balanceColumn, confidenceColumn, usageColumn; private ImageView qrCodeImageView; private AddressTextField addressTextField; private Button generateNewAddressButton; private TitledGroupBg titledGroupBg; private Label addressLabel, amountLabel; private Label qrCodeLabel; private InputTextField amountTextField; private final WalletService walletService; private final BSFormatter formatter; private final Preferences preferences; private final String paymentLabelString; private final ObservableList<DepositListItem> observableList = FXCollections.observableArrayList(); private final SortedList<DepositListItem> sortedList = new SortedList<>(observableList); private BalanceListener balanceListener; private Subscription amountTextFieldSubscription; private ChangeListener<DepositListItem> tableViewSelectionListener; private int gridRow = 0; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle /////////////////////////////////////////////////////////////////////////////////////////// @Inject private DepositView(WalletService walletService, BSFormatter formatter, Preferences preferences) { this.walletService = walletService; this.formatter = formatter; this.preferences = preferences; paymentLabelString = "Fund Bitsquare wallet"; } @Override public void initialize() { // trigger creation of at least 1 savings address walletService.getOrCreateAddressEntry(AddressEntry.Context.AVAILABLE); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); tableView.setPlaceholder(new Label("No deposit addresses have been generated yet")); tableViewSelectionListener = (observableValue, oldValue, newValue) -> { if (newValue != null) fillForm(newValue.getAddressString()); }; setSelectColumnCellFactory(); setAddressColumnCellFactory(); setBalanceColumnCellFactory(); setUsageColumnCellFactory(); setConfidenceColumnCellFactory(); addressColumn.setComparator((o1, o2) -> o1.getAddressString().compareTo(o2.getAddressString())); balanceColumn.setComparator((o1, o2) -> o1.getBalanceAsCoin().compareTo(o2.getBalanceAsCoin())); confidenceColumn.setComparator((o1, o2) -> Double.valueOf(o1.getTxConfidenceIndicator().getProgress()) .compareTo(o2.getTxConfidenceIndicator().getProgress())); usageColumn.setComparator((a, b) -> (a.getNumTxOutputs() < b.getNumTxOutputs()) ? -1 : ((a.getNumTxOutputs() == b.getNumTxOutputs()) ? 0 : 1)); tableView.getSortOrder().add(usageColumn); tableView.setItems(sortedList); titledGroupBg = addTitledGroupBg(gridPane, gridRow, 3, "Fund your wallet"); qrCodeLabel = addLabel(gridPane, gridRow, "", 0); //GridPane.setMargin(qrCodeLabel, new Insets(Layout.FIRST_ROW_DISTANCE - 9, 0, 0, 5)); qrCodeImageView = new ImageView(); qrCodeImageView.setStyle("-fx-cursor: hand;"); Tooltip.install(qrCodeImageView, new Tooltip("Open large QR-Code window")); qrCodeImageView.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute( () -> UserThread.runAfter( () -> new QRCodeWindow(getBitcoinURI()).show(), 200, TimeUnit.MILLISECONDS) )); GridPane.setRowIndex(qrCodeImageView, gridRow); GridPane.setColumnIndex(qrCodeImageView, 1); GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_DISTANCE, 0, 0, 0)); gridPane.getChildren().add(qrCodeImageView); Tuple2<Label, AddressTextField> addressTuple = addLabelAddressTextField(gridPane, ++gridRow, "Address:"); addressLabel = addressTuple.first; //GridPane.setValignment(addressLabel, VPos.TOP); //GridPane.setMargin(addressLabel, new Insets(3, 0, 0, 0)); addressTextField = addressTuple.second; addressTextField.setPaymentLabel(paymentLabelString); Tuple2<Label, InputTextField> amountTuple = addLabelInputTextField(gridPane, ++gridRow, "Amount in BTC (optional):"); amountLabel = amountTuple.first; amountTextField = amountTuple.second; if (DevFlags.DEV_MODE) amountTextField.setText("10"); titledGroupBg.setVisible(false); titledGroupBg.setManaged(false); qrCodeLabel.setVisible(false); qrCodeLabel.setManaged(false); qrCodeImageView.setVisible(false); qrCodeImageView.setManaged(false); addressLabel.setVisible(false); addressLabel.setManaged(false); addressTextField.setVisible(false); addressTextField.setManaged(false); amountLabel.setVisible(false); amountTextField.setManaged(false); generateNewAddressButton = addButton(gridPane, ++gridRow, "Generate new address", -20); GridPane.setColumnIndex(generateNewAddressButton, 0); GridPane.setHalignment(generateNewAddressButton, HPos.LEFT); generateNewAddressButton.setOnAction(event -> { boolean hasUnUsedAddress = observableList.stream().filter(e -> e.getNumTxOutputs() == 0).findAny().isPresent(); if (hasUnUsedAddress) { new Popup().warning("Please select an unused address from the table above rather than generating a new one.").show(); } else { AddressEntry newSavingsAddressEntry = walletService.getOrCreateUnusedAddressEntry(AddressEntry.Context.AVAILABLE); updateList(); observableList.stream() .filter(depositListItem -> depositListItem.getAddressString().equals(newSavingsAddressEntry.getAddressString())) .findAny() .ifPresent(depositListItem -> tableView.getSelectionModel().select(depositListItem)); } }); balanceListener = new BalanceListener() { @Override public void onBalanceChanged(Coin balance, Transaction tx) { updateList(); } }; } @Override protected void activate() { tableView.getSelectionModel().selectedItemProperty().addListener(tableViewSelectionListener); sortedList.comparatorProperty().bind(tableView.comparatorProperty()); updateList(); walletService.addBalanceListener(balanceListener); amountTextFieldSubscription = EasyBind.subscribe(amountTextField.textProperty(), t -> { addressTextField.setAmountAsCoin(formatter.parseToCoin(t)); updateQRCode(); }); if (tableView.getSelectionModel().getSelectedItem() == null && !sortedList.isEmpty()) tableView.getSelectionModel().select(0); } @Override protected void deactivate() { tableView.getSelectionModel().selectedItemProperty().removeListener(tableViewSelectionListener); sortedList.comparatorProperty().unbind(); observableList.forEach(DepositListItem::cleanup); walletService.removeBalanceListener(balanceListener); amountTextFieldSubscription.unsubscribe(); } /////////////////////////////////////////////////////////////////////////////////////////// // UI handlers /////////////////////////////////////////////////////////////////////////////////////////// private void fillForm(String address) { titledGroupBg.setVisible(true); titledGroupBg.setManaged(true); qrCodeLabel.setVisible(true); qrCodeLabel.setManaged(true); qrCodeImageView.setVisible(true); qrCodeImageView.setManaged(true); addressLabel.setVisible(true); addressLabel.setManaged(true); addressTextField.setVisible(true); addressTextField.setManaged(true); amountLabel.setVisible(true); amountTextField.setManaged(true); GridPane.setMargin(generateNewAddressButton, new Insets(15, 0, 0, 0)); addressTextField.setAddress(address); updateQRCode(); } private void updateQRCode() { if (addressTextField.getAddress() != null && !addressTextField.getAddress().isEmpty()) { final byte[] imageBytes = QRCode .from(getBitcoinURI()) .withSize(150, 150) // code has 41 elements 8 px is border with 150 we get 3x scale and min. border .to(ImageType.PNG) .stream() .toByteArray(); Image qrImage = new Image(new ByteArrayInputStream(imageBytes)); qrCodeImageView.setImage(qrImage); } } private void openBlockExplorer(DepositListItem item) { if (item.getAddressString() != null) { try { GUIUtil.openWebPage(preferences.getBlockChainExplorer().addressUrl + item.getAddressString()); } catch (Exception e) { log.error(e.getMessage()); new Popup().warning("Opening browser failed. Please check your internet " + "connection.").show(); } } } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// private void updateList() { observableList.clear(); walletService.getAvailableAddressEntries().stream() .forEach(e -> observableList.add(new DepositListItem(e, walletService, formatter))); } private Coin getAmountAsCoin() { Coin senderAmount = formatter.parseToCoin(amountTextField.getText()); if (!Restrictions.isAboveFixedTxFeeForTradesAndDust(senderAmount)) { senderAmount = Coin.ZERO; /* new Popup() .warning("The amount is lower than the transaction fee and the min. possible tx value (dust).") .show();*/ } return senderAmount; } @NotNull private String getBitcoinURI() { return BitcoinURI.convertToBitcoinURI(addressTextField.getAddress(), getAmountAsCoin(), paymentLabelString, null); } /////////////////////////////////////////////////////////////////////////////////////////// // ColumnCellFactories /////////////////////////////////////////////////////////////////////////////////////////// private void setUsageColumnCellFactory() { usageColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); usageColumn.setCellFactory(new Callback<TableColumn<DepositListItem, DepositListItem>, TableCell<DepositListItem, DepositListItem>>() { @Override public TableCell<DepositListItem, DepositListItem> call(TableColumn<DepositListItem, DepositListItem> column) { return new TableCell<DepositListItem, DepositListItem>() { @Override public void updateItem(final DepositListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(new Label(item.getUsage())); } else { setGraphic(null); } } }; } }); } private void setSelectColumnCellFactory() { selectColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); selectColumn.setCellFactory( new Callback<TableColumn<DepositListItem, DepositListItem>, TableCell<DepositListItem, DepositListItem>>() { @Override public TableCell<DepositListItem, DepositListItem> call(TableColumn<DepositListItem, DepositListItem> column) { return new TableCell<DepositListItem, DepositListItem>() { Button button; @Override public void updateItem(final DepositListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { if (button == null) { button = new Button("Select"); button.setOnAction(e -> tableView.getSelectionModel().select(item)); setGraphic(button); } } else { setGraphic(null); if (button != null) { button.setOnAction(null); button = null; } } } }; } }); } private void setAddressColumnCellFactory() { addressColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); addressColumn.setCellFactory( new Callback<TableColumn<DepositListItem, DepositListItem>, TableCell<DepositListItem, DepositListItem>>() { @Override public TableCell<DepositListItem, DepositListItem> call(TableColumn<DepositListItem, DepositListItem> column) { return new TableCell<DepositListItem, DepositListItem>() { private HyperlinkWithIcon field; @Override public void updateItem(final DepositListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { String addressString = item.getAddressString(); field = new HyperlinkWithIcon(addressString, AwesomeIcon.EXTERNAL_LINK); field.setOnAction(event -> { openBlockExplorer(item); tableView.getSelectionModel().select(item); }); field.setTooltip(new Tooltip("Open external blockchain explorer for " + "address: " + addressString)); setGraphic(field); } else { setGraphic(null); if (field != null) field.setOnAction(null); } } }; } }); } private void setBalanceColumnCellFactory() { balanceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); balanceColumn.setCellFactory(new Callback<TableColumn<DepositListItem, DepositListItem>, TableCell<DepositListItem, DepositListItem>>() { @Override public TableCell<DepositListItem, DepositListItem> call(TableColumn<DepositListItem, DepositListItem> column) { return new TableCell<DepositListItem, DepositListItem>() { @Override public void updateItem(final DepositListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { if (!textProperty().isBound()) textProperty().bind(item.balanceProperty()); } else { textProperty().unbind(); setText(""); } } }; } }); } private void setConfidenceColumnCellFactory() { confidenceColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); confidenceColumn.setCellFactory( new Callback<TableColumn<DepositListItem, DepositListItem>, TableCell<DepositListItem, DepositListItem>>() { @Override public TableCell<DepositListItem, DepositListItem> call(TableColumn<DepositListItem, DepositListItem> column) { return new TableCell<DepositListItem, DepositListItem>() { @Override public void updateItem(final DepositListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { setGraphic(item.getTxConfidenceIndicator()); } else { setGraphic(null); } } }; } }); } }