/*
* 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.account.content.seedwords;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import io.bitsquare.app.BitsquareApp;
import io.bitsquare.btc.WalletService;
import io.bitsquare.common.UserThread;
import io.bitsquare.gui.common.view.ActivatableView;
import io.bitsquare.gui.common.view.FxmlView;
import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.main.overlays.windows.WalletPasswordWindow;
import io.bitsquare.gui.util.Layout;
import io.bitsquare.user.Preferences;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.Button;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TextArea;
import javafx.scene.layout.GridPane;
import org.bitcoinj.core.Wallet;
import org.bitcoinj.crypto.KeyCrypter;
import org.bitcoinj.crypto.MnemonicCode;
import org.bitcoinj.crypto.MnemonicException;
import org.bitcoinj.wallet.DeterministicSeed;
import javax.inject.Inject;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.List;
import static io.bitsquare.gui.util.FormBuilder.*;
import static javafx.beans.binding.Bindings.createBooleanBinding;
@FxmlView
public class SeedWordsView extends ActivatableView<GridPane, Void> {
private final WalletService walletService;
private final WalletPasswordWindow walletPasswordWindow;
private Preferences preferences;
private Button restoreButton;
private TextArea displaySeedWordsTextArea, restoreSeedWordsTextArea;
private DatePicker datePicker, restoreDatePicker;
private int gridRow = 0;
private DeterministicSeed keyChainSeed;
private ChangeListener<Boolean> seedWordsValidChangeListener;
private SimpleBooleanProperty seedWordsValid = new SimpleBooleanProperty(false);
private SimpleBooleanProperty dateValid = new SimpleBooleanProperty(false);
private ChangeListener<String> seedWordsTextAreaChangeListener;
private ChangeListener<Boolean> datePickerChangeListener;
private ChangeListener<LocalDate> dateChangeListener;
private BooleanProperty seedWordsEdited = new SimpleBooleanProperty();
private String seedWordText;
private LocalDate walletCreationDate;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private SeedWordsView(WalletService walletService, WalletPasswordWindow walletPasswordWindow, Preferences preferences) {
this.walletService = walletService;
this.walletPasswordWindow = walletPasswordWindow;
this.preferences = preferences;
}
@Override
protected void initialize() {
addTitledGroupBg(root, gridRow, 2, "Backup your wallet seed words");
displaySeedWordsTextArea = addLabelTextArea(root, gridRow, "Wallet seed words:", "", Layout.FIRST_ROW_DISTANCE).second;
displaySeedWordsTextArea.setPrefHeight(60);
displaySeedWordsTextArea.setEditable(false);
datePicker = addLabelDatePicker(root, ++gridRow, "Wallet Date:").second;
datePicker.setMouseTransparent(true);
addTitledGroupBg(root, ++gridRow, 2, "Restore your wallet seed words", Layout.GROUP_DISTANCE);
restoreSeedWordsTextArea = addLabelTextArea(root, gridRow, "Wallet seed words:", "", Layout.FIRST_ROW_AND_GROUP_DISTANCE).second;
restoreSeedWordsTextArea.setPrefHeight(60);
restoreDatePicker = addLabelDatePicker(root, ++gridRow, "Wallet Date:").second;
restoreButton = addButtonAfterGroup(root, ++gridRow, "Restore wallet");
addTitledGroupBg(root, ++gridRow, 1, "Information", Layout.GROUP_DISTANCE);
addMultilineLabel(root, gridRow, "Please write down you wallet seed words and the date! " +
"You can recover your wallet any time with those seed words and the date.",
Layout.FIRST_ROW_AND_GROUP_DISTANCE);
seedWordsValidChangeListener = (observable, oldValue, newValue) -> {
if (newValue) {
restoreSeedWordsTextArea.getStyleClass().remove("validation_error");
} else {
restoreSeedWordsTextArea.getStyleClass().add("validation_error");
}
};
seedWordsTextAreaChangeListener = (observable, oldValue, newValue) -> {
seedWordsEdited.set(true);
try {
MnemonicCode codec = new MnemonicCode();
codec.check(Splitter.on(" ").splitToList(newValue));
seedWordsValid.set(true);
} catch (IOException | MnemonicException e) {
seedWordsValid.set(false);
}
};
datePickerChangeListener = (observable, oldValue, newValue) -> {
if (newValue)
restoreDatePicker.getStyleClass().remove("validation_error");
else
restoreDatePicker.getStyleClass().add("validation_error");
};
dateChangeListener = (observable, oldValue, newValue) -> dateValid.set(true);
}
@Override
public void activate() {
seedWordsValid.addListener(seedWordsValidChangeListener);
dateValid.addListener(datePickerChangeListener);
restoreSeedWordsTextArea.textProperty().addListener(seedWordsTextAreaChangeListener);
restoreDatePicker.valueProperty().addListener(dateChangeListener);
restoreButton.disableProperty().bind(createBooleanBinding(() -> !seedWordsValid.get() || !dateValid.get() || !seedWordsEdited.get(),
seedWordsValid, dateValid, seedWordsEdited));
restoreButton.setOnAction(e -> onRestore());
restoreSeedWordsTextArea.getStyleClass().remove("validation_error");
restoreDatePicker.getStyleClass().remove("validation_error");
DeterministicSeed keyChainSeed = walletService.getWallet().getKeyChainSeed();
// wallet creation date is not encrypted
walletCreationDate = Instant.ofEpochSecond(keyChainSeed.getCreationTimeSeconds()).atZone(ZoneId.systemDefault()).toLocalDate();
if (keyChainSeed.isEncrypted()) {
askForPassword();
} else {
String key = "showSeedWordsWarning";
if (preferences.showAgain(key)) {
new Popup().warning("You have not setup a wallet password which would protect the display of the seed words.\n\n" +
"Do you want to display the seed words?")
.actionButtonText("Yes, and don't ask me again")
.onAction(() -> {
preferences.dontShowAgain(key, true);
initSeedWords(keyChainSeed);
showSeedScreen();
})
.closeButtonText("No")
.show();
} else {
initSeedWords(keyChainSeed);
showSeedScreen();
}
}
}
@Override
protected void deactivate() {
seedWordsValid.removeListener(seedWordsValidChangeListener);
dateValid.removeListener(datePickerChangeListener);
restoreSeedWordsTextArea.textProperty().removeListener(seedWordsTextAreaChangeListener);
restoreDatePicker.valueProperty().removeListener(dateChangeListener);
restoreButton.disableProperty().unbind();
restoreButton.setOnAction(null);
displaySeedWordsTextArea.setText("");
restoreSeedWordsTextArea.setText("");
restoreDatePicker.setValue(null);
datePicker.setValue(null);
restoreSeedWordsTextArea.getStyleClass().remove("validation_error");
restoreDatePicker.getStyleClass().remove("validation_error");
}
private void askForPassword() {
walletPasswordWindow.headLine("Enter password to view seed words").onAesKey(aesKey -> {
Wallet wallet = walletService.getWallet();
KeyCrypter keyCrypter = wallet.getKeyCrypter();
keyChainSeed = wallet.getKeyChainSeed();
if (keyCrypter != null) {
DeterministicSeed decryptedSeed = keyChainSeed.decrypt(keyCrypter, "", aesKey);
initSeedWords(decryptedSeed);
showSeedScreen();
} else {
log.warn("keyCrypter is null");
}
}).show();
}
private void initSeedWords(DeterministicSeed seed) {
List<String> mnemonicCode = seed.getMnemonicCode();
if (mnemonicCode != null) {
seedWordText = Joiner.on(" ").join(mnemonicCode);
}
}
private void showSeedScreen() {
displaySeedWordsTextArea.setText(seedWordText);
datePicker.setValue(walletCreationDate);
}
private void onRestore() {
Wallet wallet = walletService.getWallet();
if (wallet.getBalance(Wallet.BalanceType.AVAILABLE).value > 0) {
new Popup()
.warning("Your bitcoin wallet is not empty.\n\n" +
"You must empty this wallet before attempting to restore an older one, as mixing wallets " +
"together can lead to invalidated backups.\n\n" +
"Please finalize your trades, close all your open offers and go to the Funds section to withdraw your bitcoin.\n" +
"In case you cannot access your bitcoin you can use the emergency tool to empty the wallet.\n" +
"To open that emergency tool press \"cmd + e\".")
.actionButtonText("I want to restore anyway")
.onAction(this::checkIfEncrypted)
.closeButtonText("I will empty my wallet first")
.show();
} else {
checkIfEncrypted();
}
}
private void checkIfEncrypted() {
if (walletService.getWallet().isEncrypted()) {
new Popup()
.information("Your bitcoin wallet is encrypted.\n\n" +
"After restore, the wallet will no longer be encrypted and you must set a new password.\n\n" +
"Do you want to proceed?")
.closeButtonText("No")
.actionButtonText("Yes")
.onAction(this::doRestore)
.show();
} else {
doRestore();
}
}
private void doRestore() {
long date = restoreDatePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC);
DeterministicSeed seed = new DeterministicSeed(Splitter.on(" ").splitToList(restoreSeedWordsTextArea.getText()), null, "", date);
walletService.restoreSeedWords(seed,
() -> UserThread.execute(() -> {
log.info("Wallet restored with seed words");
new Popup()
.feedback("Wallet restored successfully with the new seed words.\n\n" +
"You need to shut down and restart the application.")
.closeButtonText("Shut down")
.onClose(BitsquareApp.shutDownHandler::run).show();
}),
throwable -> UserThread.execute(() -> {
log.error(throwable.getMessage());
new Popup()
.error("An error occurred when restoring the wallet with seed words.\n" +
"Error message: " + throwable.getMessage())
.show();
}));
}
}