package lighthouse.subwindows;
import com.google.common.base.*;
import javafx.application.Platform;
import javafx.beans.binding.*;
import javafx.event.*;
import javafx.fxml.*;
import javafx.scene.control.*;
import lighthouse.*;
import lighthouse.utils.*;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.*;
import org.bitcoinj.wallet.*;
import org.slf4j.*;
import org.spongycastle.crypto.params.*;
import javax.annotation.*;
import java.time.*;
import java.util.*;
import static com.google.common.base.Preconditions.*;
import static javafx.beans.binding.Bindings.*;
import static lighthouse.protocol.LHUtils.*;
import static lighthouse.utils.GuiUtils.*;
import static lighthouse.utils.I18nUtil.*;
public class WalletSettingsController {
private static final Logger log = LoggerFactory.getLogger(WalletSettingsController.class);
@FXML Button passwordButton;
@FXML DatePicker datePicker;
@FXML TextArea wordsArea;
@FXML Button restoreButton;
public Main.OverlayUI overlayUI;
private KeyParameter aesKey;
public static void open(@Nullable KeyParameter key) {
checkGuiThread();
Main.OverlayUI<WalletSettingsController> screen = Main.instance.overlayUI("subwindows/wallet_settings.fxml", tr("Wallet settings"));
screen.controller.initialize(key);
}
// Note: NOT called by FXMLLoader!
public void initialize(@Nullable KeyParameter aesKey) {
DeterministicSeed seed = Main.bitcoin.wallet().getKeyChainSeed();
if (aesKey == null) {
if (seed.isEncrypted()) {
log.info("Wallet is encrypted, requesting password first.");
// Delay execution of this until after we've finished initialising this screen.
Platform.runLater(this::askForPasswordAndRetry);
return;
}
} else {
this.aesKey = aesKey;
seed = seed.decrypt(checkNotNull(Main.bitcoin.wallet().getKeyCrypter()), "", aesKey);
// Now we can display the wallet seed as appropriate.
passwordButton.setText(tr("Remove password"));
}
// Set the date picker to show the birthday of this wallet.
Instant creationTime = Instant.ofEpochSecond(seed.getCreationTimeSeconds());
LocalDate origDate = creationTime.atZone(ZoneId.systemDefault()).toLocalDate();
datePicker.setValue(origDate);
// Set the mnemonic seed words.
final List<String> mnemonicCode = seed.getMnemonicCode();
checkNotNull(mnemonicCode); // Already checked for encryption.
String origWords = Joiner.on(" ").join(mnemonicCode);
wordsArea.setText(origWords);
// Validate words as they are being typed.
MnemonicCode codec = unchecked(MnemonicCode::new);
TextFieldValidator validator = new TextFieldValidator(wordsArea, text ->
!didThrow(() -> codec.check(Splitter.on(' ').splitToList(text)))
);
// Clear the date picker if the user starts editing the words, if it contained the current wallets date.
// This forces them to set the birthday field when restoring.
wordsArea.textProperty().addListener(o -> {
if (origDate.equals(datePicker.getValue()))
datePicker.setValue(null);
});
BooleanBinding datePickerIsInvalid = or(
datePicker.valueProperty().isNull(),
createBooleanBinding(() ->
datePicker.getValue().isAfter(LocalDate.now())
, /* depends on */ datePicker.valueProperty())
);
// Don't let the user click restore if the words area contains the current wallet words, or are an invalid set,
// or if the date field isn't set, or if it's in the future.
restoreButton.disableProperty().bind(
or(
or(
not(validator.valid),
equal(origWords, wordsArea.textProperty())
),
datePickerIsInvalid
)
);
// Highlight the date picker in red if it's empty or in the future, so the user knows why restore is disabled.
datePickerIsInvalid.addListener((dp, old, cur) -> {
if (cur) {
datePicker.getStyleClass().add("validation_error");
} else {
datePicker.getStyleClass().remove("validation_error");
}
});
}
private void askForPasswordAndRetry() {
WalletPasswordController.requestPasswordWithNextWindow(WalletSettingsController::open);
}
@FXML
public void closeClicked(ActionEvent event) {
overlayUI.done();
}
@FXML
public void restoreClicked(ActionEvent event) {
// Don't allow a restore unless this wallet is presently empty. We don't want to end up with two wallets, too
// much complexity, even though WalletAppKit will keep the current one as a backup file in case of disaster.
if (Main.bitcoin.wallet().getBalance(Wallet.BalanceType.AVAILABLE_SPENDABLE).value > 0) {
informationalAlert(tr("Wallet is not empty"),
tr("You must empty this wallet out before attempting to restore an older one, as mixing wallets " +
"together can lead to invalidated backups."));
return;
}
if (Main.bitcoin.isOffline()) {
informationalAlert(tr("You are offline"),
tr("You cannot restore your wallet from seed words when offline. Go online and restart the app first."));
return;
}
if (aesKey != null) {
// This is weak. We should encrypt the new seed here.
informationalAlert(tr("Wallet is encrypted"),
tr("After restore, the wallet will no longer be encrypted and you must set a new password."));
}
log.info("Attempting wallet restore using seed '{}' from date {}", wordsArea.getText(), datePicker.getValue());
informationalAlert(tr("Wallet restore in progress"),
tr("Your wallet will now be resynced from the Bitcoin network. This can take a long time for old wallets."));
overlayUI.done();
long birthday = datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC);
DeterministicSeed seed = new DeterministicSeed(Splitter.on(' ').splitToList(wordsArea.getText()), null, "", birthday);
// Shut down bitcoinj and restart it with the new seed.
Main.bitcoin.restoreFromSeed(seed);
MainWindow.bitcoinUIModel.setWallet(Main.bitcoin.getWallet());
}
@FXML
public void passwordButtonClicked(ActionEvent event) {
if (aesKey == null) {
Main.instance.overlayUI("subwindows/wallet_set_password.fxml", tr("Set password"));
} else {
Main.bitcoin.wallet().decrypt(aesKey);
informationalAlert(tr("Wallet decrypted"), tr("A password will no longer be required to send money or edit settings."));
passwordButton.setText(tr("Set password"));
aesKey = null;
}
}
}