/*
* 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);
}
}
};
}
});
}
}