mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-01-31 07:12:17 +00:00
WalletTemplate: add support for encryption. Scrypt parameters are calculated by testing CPU speed. A pie chart shows smooth decryption progress.
This commit is contained in:
parent
d6cf090f5c
commit
29a11e22b7
@ -1,6 +1,9 @@
|
||||
package wallettemplate;
|
||||
|
||||
import com.google.bitcoin.core.*;
|
||||
import com.google.bitcoin.core.AbstractWalletEventListener;
|
||||
import com.google.bitcoin.core.Coin;
|
||||
import com.google.bitcoin.core.DownloadListener;
|
||||
import com.google.bitcoin.core.Wallet;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
@ -47,7 +50,8 @@ public class Controller {
|
||||
}
|
||||
|
||||
public void settingsClicked(ActionEvent event) {
|
||||
Main.instance.overlayUI("wallet_settings.fxml");
|
||||
Main.OverlayUI<WalletSettingsController> screen = Main.instance.overlayUI("wallet_settings.fxml");
|
||||
screen.controller.initialize(null);
|
||||
}
|
||||
|
||||
public class ProgressBarUpdater extends DownloadListener {
|
||||
@ -104,6 +108,7 @@ public class Controller {
|
||||
public void onWalletChanged(Wallet wallet) {
|
||||
checkGuiThread();
|
||||
refreshBalanceLabel();
|
||||
// TODO: Refresh clickable address here.
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ import static wallettemplate.utils.GuiUtils.*;
|
||||
public class Main extends Application {
|
||||
public static String APP_NAME = "WalletTemplate";
|
||||
|
||||
public static NetworkParameters params = MainNetParams.get();
|
||||
public static NetworkParameters params = RegTestParams.get();
|
||||
public static WalletAppKit bitcoin;
|
||||
public static Main instance;
|
||||
|
||||
@ -90,7 +90,8 @@ public class Main extends Application {
|
||||
// Don't make the user wait for confirmations for now, as the intention is they're sending it
|
||||
// their own money!
|
||||
bitcoin.wallet().allowSpendingUnconfirmedTransactions();
|
||||
bitcoin.peerGroup().setMaxConnections(11);
|
||||
if (params != RegTestParams.get())
|
||||
bitcoin.peerGroup().setMaxConnections(11);
|
||||
bitcoin.peerGroup().setBloomFilterFalsePositiveRate(0.00001);
|
||||
Platform.runLater(controller::onBitcoinSetup);
|
||||
}
|
||||
@ -115,6 +116,8 @@ public class Main extends Application {
|
||||
bitcoin.restoreWalletFromSeed(seed);
|
||||
}
|
||||
|
||||
private Node stopClickPane = new Pane();
|
||||
|
||||
public class OverlayUI<T> {
|
||||
public Node ui;
|
||||
public T controller;
|
||||
@ -125,26 +128,53 @@ public class Main extends Application {
|
||||
}
|
||||
|
||||
public void show() {
|
||||
blurOut(mainUI);
|
||||
uiStack.getChildren().add(ui);
|
||||
fadeIn(ui);
|
||||
checkGuiThread();
|
||||
if (currentOverlay == null) {
|
||||
uiStack.getChildren().add(stopClickPane);
|
||||
uiStack.getChildren().add(ui);
|
||||
blurOut(mainUI);
|
||||
//darken(mainUI);
|
||||
fadeIn(ui);
|
||||
zoomIn(ui);
|
||||
} else {
|
||||
// Do a quick transition between the current overlay and the next.
|
||||
// Bug here: we don't pay attention to changes in outsideClickDismisses.
|
||||
explodeOut(currentOverlay.ui);
|
||||
fadeOutAndRemove(uiStack, currentOverlay.ui);
|
||||
uiStack.getChildren().add(ui);
|
||||
ui.setOpacity(0.0);
|
||||
fadeIn(ui, 100);
|
||||
zoomIn(ui, 100);
|
||||
}
|
||||
currentOverlay = this;
|
||||
}
|
||||
|
||||
public void outsideClickDismisses() {
|
||||
stopClickPane.setOnMouseClicked((ev) -> done());
|
||||
}
|
||||
|
||||
public void done() {
|
||||
checkGuiThread();
|
||||
fadeOutAndRemove(ui, uiStack);
|
||||
if (ui == null) return; // In the middle of being dismissed and got an extra click.
|
||||
explodeOut(ui);
|
||||
fadeOutAndRemove(uiStack, ui, stopClickPane);
|
||||
blurIn(mainUI);
|
||||
//undark(mainUI);
|
||||
this.ui = null;
|
||||
this.controller = null;
|
||||
currentOverlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private OverlayUI currentOverlay;
|
||||
|
||||
public <T> OverlayUI<T> overlayUI(Node node, T controller) {
|
||||
checkGuiThread();
|
||||
OverlayUI<T> pair = new OverlayUI<T>(node, controller);
|
||||
// Auto-magically set the overlayUi member, if it's there.
|
||||
// Auto-magically set the overlayUI member, if it's there.
|
||||
try {
|
||||
controller.getClass().getDeclaredField("overlayUi").set(controller, pair);
|
||||
controller.getClass().getField("overlayUI").set(controller, pair);
|
||||
} catch (IllegalAccessException | NoSuchFieldException ignored) {
|
||||
}
|
||||
pair.show();
|
||||
@ -156,15 +186,17 @@ public class Main extends Application {
|
||||
try {
|
||||
checkGuiThread();
|
||||
// Load the UI from disk.
|
||||
URL location = getClass().getResource(name);
|
||||
URL location = GuiUtils.getResource(name);
|
||||
FXMLLoader loader = new FXMLLoader(location);
|
||||
Pane ui = loader.load();
|
||||
T controller = loader.getController();
|
||||
OverlayUI<T> pair = new OverlayUI<T>(ui, controller);
|
||||
// Auto-magically set the overlayUi member, if it's there.
|
||||
// Auto-magically set the overlayUI member, if it's there.
|
||||
try {
|
||||
controller.getClass().getDeclaredField("overlayUi").set(controller, pair);
|
||||
if (controller != null)
|
||||
controller.getClass().getField("overlayUI").set(controller, pair);
|
||||
} catch (IllegalAccessException | NoSuchFieldException ignored) {
|
||||
ignored.printStackTrace();
|
||||
}
|
||||
pair.show();
|
||||
return pair;
|
||||
|
@ -3,15 +3,15 @@ package wallettemplate;
|
||||
import com.google.bitcoin.core.*;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextField;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
import wallettemplate.controls.BitcoinAddressValidator;
|
||||
|
||||
import static wallettemplate.utils.GuiUtils.crashAlert;
|
||||
import static wallettemplate.utils.GuiUtils.informationalAlert;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static wallettemplate.utils.GuiUtils.*;
|
||||
|
||||
public class SendMoneyController {
|
||||
public Button sendBtn;
|
||||
@ -19,28 +19,33 @@ public class SendMoneyController {
|
||||
public TextField address;
|
||||
public Label titleLabel;
|
||||
|
||||
public Main.OverlayUI overlayUi;
|
||||
public Main.OverlayUI overlayUI;
|
||||
|
||||
private Wallet.SendResult sendResult;
|
||||
private KeyParameter aesKey;
|
||||
|
||||
// Called by FXMLLoader
|
||||
public void initialize() {
|
||||
checkState(!Main.bitcoin.wallet().getBalance().isZero());
|
||||
new BitcoinAddressValidator(Main.params, address, sendBtn);
|
||||
}
|
||||
|
||||
public void cancel(ActionEvent event) {
|
||||
overlayUi.done();
|
||||
overlayUI.done();
|
||||
}
|
||||
|
||||
public void send(ActionEvent event) {
|
||||
// Address exception cannot happen as we validated it beforehand.
|
||||
try {
|
||||
Address destination = new Address(Main.params, address.getText());
|
||||
Wallet.SendRequest req = Wallet.SendRequest.emptyWallet(destination);
|
||||
req.aesKey = aesKey;
|
||||
sendResult = Main.bitcoin.wallet().sendCoins(req);
|
||||
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() {
|
||||
@Override
|
||||
public void onSuccess(Transaction result) {
|
||||
Platform.runLater(overlayUi::done);
|
||||
checkGuiThread();
|
||||
overlayUI.done();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -56,16 +61,32 @@ public class SendMoneyController {
|
||||
sendBtn.setDisable(true);
|
||||
address.setDisable(true);
|
||||
updateTitleForBroadcast();
|
||||
} catch (AddressFormatException e) {
|
||||
// Cannot happen because we already validated it when the text field changed.
|
||||
throw new RuntimeException(e);
|
||||
} catch (InsufficientMoneyException e) {
|
||||
informationalAlert("Could not empty the wallet",
|
||||
"You may have too little money left in the wallet to make a transaction.");
|
||||
overlayUi.done();
|
||||
overlayUI.done();
|
||||
} catch (ECKey.KeyIsEncryptedException e) {
|
||||
askForPasswordAndRetry();
|
||||
} catch (AddressFormatException e) {
|
||||
// Cannot happen because we already validated it when the text field changed.
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void askForPasswordAndRetry() {
|
||||
Main.OverlayUI<WalletPasswordController> pwd = Main.instance.overlayUI("wallet_password.fxml");
|
||||
final String addressStr = address.getText();
|
||||
pwd.controller.aesKeyProperty().addListener((observable, old, cur) -> {
|
||||
// We only get here if the user found the right password. If they don't or they cancel, we end up back on
|
||||
// the main UI screen. By now the send money screen is history so we must recreate it.
|
||||
checkGuiThread();
|
||||
Main.OverlayUI<SendMoneyController> screen = Main.instance.overlayUI("send_money.fxml");
|
||||
screen.controller.aesKey = cur;
|
||||
screen.controller.address.setText(addressStr);
|
||||
screen.controller.send(null);
|
||||
});
|
||||
}
|
||||
|
||||
private void updateTitleForBroadcast() {
|
||||
final int peers = sendResult.tx.getConfidence().numBroadcastPeers();
|
||||
titleLabel.setText(String.format("Broadcasting ... seen by %d peers", peers));
|
||||
|
@ -0,0 +1,106 @@
|
||||
package wallettemplate;
|
||||
|
||||
import com.google.bitcoin.crypto.KeyCrypterScrypt;
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.google.protobuf.ByteString;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
import wallettemplate.utils.KeyDerivationTasks;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static wallettemplate.utils.GuiUtils.*;
|
||||
|
||||
/**
|
||||
* User interface for entering a password on demand, e.g. to send money. Also used when encrypting a wallet. Shows a
|
||||
* progress meter as we scrypt the password.
|
||||
*/
|
||||
public class WalletPasswordController {
|
||||
private static final Logger log = LoggerFactory.getLogger(WalletPasswordController.class);
|
||||
|
||||
@FXML HBox buttonsBox;
|
||||
@FXML PasswordField pass1;
|
||||
@FXML ImageView padlockImage;
|
||||
@FXML ProgressIndicator progressMeter;
|
||||
@FXML GridPane widgetGrid;
|
||||
@FXML Label explanationLabel;
|
||||
|
||||
public Main.OverlayUI overlayUI;
|
||||
|
||||
private SimpleObjectProperty<KeyParameter> aesKey = new SimpleObjectProperty<>();
|
||||
|
||||
public void initialize() {
|
||||
progressMeter.setOpacity(0);
|
||||
}
|
||||
|
||||
@FXML void confirmClicked(ActionEvent event) {
|
||||
String password = pass1.getText();
|
||||
if (password.isEmpty() || password.length() < 4) {
|
||||
informationalAlert("Bad password", "The password you entered is empty or too short.");
|
||||
return;
|
||||
}
|
||||
|
||||
final KeyCrypterScrypt keyCrypter = (KeyCrypterScrypt) Main.bitcoin.wallet().getKeyCrypter();
|
||||
checkNotNull(keyCrypter); // We should never arrive at this GUI if the wallet isn't actually encrypted.
|
||||
KeyDerivationTasks tasks = new KeyDerivationTasks(keyCrypter, password, getTargetTime()) {
|
||||
@Override
|
||||
protected void onFinish(KeyParameter aesKey) {
|
||||
super.onFinish(aesKey);
|
||||
checkGuiThread();
|
||||
if (Main.bitcoin.wallet().checkAESKey(aesKey)) {
|
||||
WalletPasswordController.this.aesKey.set(aesKey);
|
||||
} else {
|
||||
log.warn("User entered incorrect password");
|
||||
fadeOut(progressMeter);
|
||||
fadeIn(widgetGrid);
|
||||
fadeIn(explanationLabel);
|
||||
fadeIn(buttonsBox);
|
||||
informationalAlert("Wrong password",
|
||||
"Please try entering your password again, carefully checking for typos or spelling errors.");
|
||||
}
|
||||
}
|
||||
};
|
||||
progressMeter.progressProperty().bind(tasks.progress);
|
||||
tasks.start();
|
||||
|
||||
fadeIn(progressMeter);
|
||||
fadeOut(widgetGrid);
|
||||
fadeOut(explanationLabel);
|
||||
fadeOut(buttonsBox);
|
||||
}
|
||||
|
||||
public void cancelClicked(ActionEvent event) {
|
||||
overlayUI.done();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<KeyParameter> aesKeyProperty() {
|
||||
return aesKey;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static final String TAG = WalletPasswordController.class.getName() + ".target-time";
|
||||
|
||||
// Writes the given time to the wallet as a tag so we can find it again in this class.
|
||||
public static void setTargetTime(Duration targetTime) {
|
||||
ByteString bytes = ByteString.copyFrom(Longs.toByteArray(targetTime.toMillis()));
|
||||
Main.bitcoin.wallet().setTag(TAG, bytes);
|
||||
}
|
||||
|
||||
// Reads target time or throws if not set yet (should never happen).
|
||||
public static Duration getTargetTime() throws IllegalArgumentException {
|
||||
return Duration.ofMillis(Longs.fromByteArray(Main.bitcoin.wallet().getTag(TAG).toByteArray()));
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package wallettemplate;
|
||||
|
||||
import com.google.bitcoin.crypto.KeyCrypterScrypt;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
import wallettemplate.utils.KeyDerivationTasks;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import static wallettemplate.utils.GuiUtils.*;
|
||||
|
||||
public class WalletSetPasswordController {
|
||||
private static final Logger log = LoggerFactory.getLogger(WalletSetPasswordController.class);
|
||||
public PasswordField pass1, pass2;
|
||||
|
||||
public ImageView padlockImage;
|
||||
public ProgressIndicator progressMeter;
|
||||
public GridPane widgetGrid;
|
||||
public Button closeButton;
|
||||
public Label explanationLabel;
|
||||
|
||||
public Main.OverlayUI overlayUI;
|
||||
|
||||
public void initialize() {
|
||||
padlockImage.setOpacity(0);
|
||||
progressMeter.setOpacity(0);
|
||||
}
|
||||
|
||||
public void setPasswordClicked(ActionEvent event) {
|
||||
if (!pass1.getText().equals(pass2.getText())) {
|
||||
informationalAlert("Passwords do not match", "Try re-typing your chosen passwords.");
|
||||
return;
|
||||
}
|
||||
String password = pass1.getText();
|
||||
// This is kind of arbitrary and we could do much more to help people pick strong passwords.
|
||||
if (password.length() < 4) {
|
||||
informationalAlert("Password too short", "You need to pick a password at least five characters or longer.");
|
||||
return;
|
||||
}
|
||||
|
||||
fadeIn(progressMeter);
|
||||
fadeIn(padlockImage);
|
||||
fadeOut(widgetGrid);
|
||||
fadeOut(explanationLabel);
|
||||
fadeOut(closeButton);
|
||||
|
||||
// Figure out how fast this computer can scrypt. We do it on the UI thread because the delay should be small
|
||||
// and so we don't really care about blocking here.
|
||||
IdealPasswordParameters params = new IdealPasswordParameters(password);
|
||||
KeyCrypterScrypt scrypt = new KeyCrypterScrypt(params.realIterations);
|
||||
// Write the target time to the wallet so we can make the progress bar work when entering the password.
|
||||
WalletPasswordController.setTargetTime(params.realTargetTime);
|
||||
|
||||
// Deriving the actual key runs on a background thread.
|
||||
KeyDerivationTasks tasks = new KeyDerivationTasks(scrypt, password, params.realTargetTime) {
|
||||
@Override
|
||||
protected void onFinish(KeyParameter aesKey) {
|
||||
// The actual encryption part doesn't take very long as most private keys are derived on demand.
|
||||
Main.bitcoin.wallet().encrypt(scrypt, aesKey);
|
||||
fadeIn(explanationLabel);
|
||||
fadeIn(widgetGrid);
|
||||
fadeIn(closeButton);
|
||||
fadeOut(progressMeter);
|
||||
fadeOut(padlockImage);
|
||||
}
|
||||
};
|
||||
progressMeter.progressProperty().bind(tasks.progress);
|
||||
tasks.start();
|
||||
}
|
||||
|
||||
public void closeClicked(ActionEvent event) {
|
||||
overlayUI.done();
|
||||
}
|
||||
|
||||
private static class IdealPasswordParameters {
|
||||
public final int realIterations;
|
||||
public final Duration realTargetTime;
|
||||
|
||||
public IdealPasswordParameters(String password) {
|
||||
final int targetTimeMsec = 2000;
|
||||
|
||||
int iterations = 16384;
|
||||
KeyCrypterScrypt scrypt = new KeyCrypterScrypt(iterations);
|
||||
long now = System.currentTimeMillis();
|
||||
scrypt.deriveKey(password);
|
||||
long time = System.currentTimeMillis() - now;
|
||||
log.info("Initial iterations took {} msec", time);
|
||||
|
||||
// N can only be a power of two, so we keep shifting both iterations and doubling time taken
|
||||
// until we are in sorta the right general area.
|
||||
while (time < targetTimeMsec) {
|
||||
iterations <<= 1;
|
||||
time *= 2;
|
||||
}
|
||||
|
||||
realIterations = iterations;
|
||||
// Fudge it by +10% to ensure our progress meter is always a bit behind the real encryption. Plus
|
||||
// without this it seems the real scrypting always takes a bit longer than we estimated for some reason.
|
||||
realTargetTime = Duration.ofMillis((long) (time * 1.1));
|
||||
}
|
||||
}
|
||||
}
|
@ -14,14 +14,19 @@ import javafx.scene.control.DatePicker;
|
||||
import javafx.scene.control.TextArea;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
import wallettemplate.utils.TextFieldValidator;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static javafx.beans.binding.Bindings.*;
|
||||
import static wallettemplate.utils.GuiUtils.checkGuiThread;
|
||||
import static wallettemplate.utils.GuiUtils.informationalAlert;
|
||||
import static wallettemplate.utils.WTUtils.didThrow;
|
||||
import static wallettemplate.utils.WTUtils.unchecked;
|
||||
@ -29,15 +34,31 @@ import static wallettemplate.utils.WTUtils.unchecked;
|
||||
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;
|
||||
public Main.OverlayUI overlayUI;
|
||||
|
||||
// Called by FXMLLoader
|
||||
public void initialize() {
|
||||
private KeyParameter aesKey;
|
||||
|
||||
// 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(() -> 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("Remove password");
|
||||
}
|
||||
|
||||
// Set the date picker to show the birthday of this wallet.
|
||||
Instant creationTime = Instant.ofEpochSecond(seed.getCreationTimeSeconds());
|
||||
@ -45,7 +66,9 @@ public class WalletSettingsController {
|
||||
datePicker.setValue(origDate);
|
||||
|
||||
// Set the mnemonic seed words.
|
||||
String origWords = Joiner.on(" ").join(seed.getMnemonicCode());
|
||||
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.
|
||||
@ -92,8 +115,19 @@ public class WalletSettingsController {
|
||||
});
|
||||
}
|
||||
|
||||
private void askForPasswordAndRetry() {
|
||||
Main.OverlayUI<WalletPasswordController> pwd = Main.instance.overlayUI("wallet_password.fxml");
|
||||
pwd.controller.aesKeyProperty().addListener((observable, old, cur) -> {
|
||||
// We only get here if the user found the right password. If they don't or they cancel, we end up back on
|
||||
// the main UI screen.
|
||||
checkGuiThread();
|
||||
Main.OverlayUI<WalletSettingsController> screen = Main.instance.overlayUI("wallet_settings.fxml");
|
||||
screen.controller.initialize(cur);
|
||||
});
|
||||
}
|
||||
|
||||
public void closeClicked(ActionEvent event) {
|
||||
overlayUi.done();
|
||||
overlayUI.done();
|
||||
}
|
||||
|
||||
public void restoreClicked(ActionEvent event) {
|
||||
@ -106,10 +140,16 @@ public class WalletSettingsController {
|
||||
return;
|
||||
}
|
||||
|
||||
if (aesKey != null) {
|
||||
// This is weak. We should encrypt the new seed here.
|
||||
informationalAlert("Wallet is encrypted",
|
||||
"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("Wallet restore in progress",
|
||||
"Your wallet will now be resynced from the Bitcoin network. This can take a long time for old wallets.");
|
||||
overlayUi.done();
|
||||
overlayUI.done();
|
||||
Main.instance.controller.restoreFromSeedAnimation();
|
||||
|
||||
long birthday = datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC);
|
||||
@ -124,4 +164,15 @@ public class WalletSettingsController {
|
||||
}, Platform::runLater);
|
||||
Main.bitcoin.stopAsync();
|
||||
}
|
||||
|
||||
|
||||
public void passwordButtonClicked(ActionEvent event) {
|
||||
if (aesKey == null) {
|
||||
Main.instance.overlayUI("wallet_set_password.fxml");
|
||||
} else {
|
||||
Main.bitcoin.wallet().decrypt(aesKey);
|
||||
informationalAlert("Wallet decrypted", "A password will no longer be required to send money or edit settings.");
|
||||
passwordButton.setText("Set password");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,11 +11,14 @@ import javafx.scene.layout.Pane;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import wallettemplate.Controller;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static wallettemplate.utils.WTUtils.unchecked;
|
||||
|
||||
public class GuiUtils {
|
||||
public static void runAlert(BiConsumer<Stage, AlertWindowController> setup) {
|
||||
@ -66,13 +69,21 @@ public class GuiUtils {
|
||||
Platform.runLater(r);
|
||||
}
|
||||
|
||||
private static final int UI_ANIMATION_TIME_MSEC = 350;
|
||||
private static final int UI_ANIMATION_TIME_MSEC = 600;
|
||||
|
||||
public static void fadeIn(Node ui) {
|
||||
public static Animation fadeIn(Node ui) {
|
||||
return fadeIn(ui, 0);
|
||||
}
|
||||
|
||||
public static Animation fadeIn(Node ui, int delayMillis) {
|
||||
ui.setCache(true);
|
||||
FadeTransition ft = new FadeTransition(Duration.millis(UI_ANIMATION_TIME_MSEC), ui);
|
||||
ft.setFromValue(0.0);
|
||||
ft.setToValue(1.0);
|
||||
ft.setOnFinished(ev -> ui.setCache(false));
|
||||
ft.setDelay(Duration.millis(delayMillis));
|
||||
ft.play();
|
||||
return ft;
|
||||
}
|
||||
|
||||
public static Animation fadeOut(Node ui) {
|
||||
@ -83,12 +94,22 @@ public class GuiUtils {
|
||||
return ft;
|
||||
}
|
||||
|
||||
public static Animation fadeOutAndRemove(Node ui, Pane parentPane) {
|
||||
Animation animation = fadeOut(ui);
|
||||
animation.setOnFinished(actionEvent -> parentPane.getChildren().remove(ui));
|
||||
public static Animation fadeOutAndRemove(Pane parentPane, Node... nodes) {
|
||||
Animation animation = fadeOut(nodes[0]);
|
||||
animation.setOnFinished(actionEvent -> parentPane.getChildren().removeAll(nodes));
|
||||
return animation;
|
||||
}
|
||||
|
||||
public static Animation fadeOutAndRemove(Duration duration, Pane parentPane, Node... nodes) {
|
||||
nodes[0].setCache(true);
|
||||
FadeTransition ft = new FadeTransition(duration, nodes[0]);
|
||||
ft.setFromValue(nodes[0].getOpacity());
|
||||
ft.setToValue(0.0);
|
||||
ft.setOnFinished(actionEvent -> parentPane.getChildren().removeAll(nodes));
|
||||
ft.play();
|
||||
return ft;
|
||||
}
|
||||
|
||||
public static void blurOut(Node node) {
|
||||
GaussianBlur blur = new GaussianBlur(0.0);
|
||||
node.setEffect(blur);
|
||||
@ -109,6 +130,40 @@ public class GuiUtils {
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
public static ScaleTransition zoomIn(Node node) {
|
||||
return zoomIn(node, 0);
|
||||
}
|
||||
|
||||
public static ScaleTransition zoomIn(Node node, int delayMillis) {
|
||||
return scaleFromTo(node, 0.95, 1.0, delayMillis);
|
||||
}
|
||||
|
||||
public static ScaleTransition explodeOut(Node node) {
|
||||
return scaleFromTo(node, 1.0, 1.05, 0);
|
||||
}
|
||||
|
||||
private static ScaleTransition scaleFromTo(Node node, double from, double to, int delayMillis) {
|
||||
ScaleTransition scale = new ScaleTransition(Duration.millis(UI_ANIMATION_TIME_MSEC / 2), node);
|
||||
scale.setFromX(from);
|
||||
scale.setFromY(from);
|
||||
scale.setToX(to);
|
||||
scale.setToY(to);
|
||||
scale.setDelay(Duration.millis(delayMillis));
|
||||
scale.play();
|
||||
return scale;
|
||||
}
|
||||
|
||||
/**
|
||||
* A useful helper for development purposes. Used as a switch for loading files from local disk, allowing live
|
||||
* editing whilst the app runs without rebuilds.
|
||||
*/
|
||||
public static URL getResource(String name) {
|
||||
if (false)
|
||||
return unchecked(() -> new URL("file:///your/path/here/src/main/wallettemplate/" + name));
|
||||
else
|
||||
return Controller.class.getResource(name);
|
||||
}
|
||||
|
||||
public static void checkGuiThread() {
|
||||
checkState(Platform.isFxApplicationThread());
|
||||
}
|
||||
|
@ -0,0 +1,80 @@
|
||||
package wallettemplate.utils;
|
||||
|
||||
import com.google.bitcoin.crypto.KeyCrypterScrypt;
|
||||
import com.google.common.util.concurrent.Uninterruptibles;
|
||||
import javafx.beans.property.ReadOnlyDoubleProperty;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static wallettemplate.utils.GuiUtils.checkGuiThread;
|
||||
|
||||
/**
|
||||
* Background tasks for pumping a progress meter and deriving an AES key using scrypt.
|
||||
*/
|
||||
public class KeyDerivationTasks {
|
||||
private static final Logger log = LoggerFactory.getLogger(KeyDerivationTasks.class);
|
||||
|
||||
public final Task<KeyParameter> keyDerivationTask;
|
||||
public final ReadOnlyDoubleProperty progress;
|
||||
|
||||
private final Task<Void> progressTask;
|
||||
|
||||
public KeyDerivationTasks(KeyCrypterScrypt scrypt, String password, Duration targetTime) {
|
||||
keyDerivationTask = new Task<KeyParameter>() {
|
||||
@Override
|
||||
protected KeyParameter call() throws Exception {
|
||||
try {
|
||||
return scrypt.deriveKey(password);
|
||||
} catch (Throwable e) {
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
} finally {
|
||||
log.info("Key derivation done");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// And the fake progress meter ...
|
||||
progressTask = new Task<Void>() {
|
||||
private KeyParameter aesKey;
|
||||
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
long startTime = System.currentTimeMillis();
|
||||
long curTime;
|
||||
long targetTimeMillis = targetTime.toMillis();
|
||||
while ((curTime = System.currentTimeMillis()) < startTime + targetTimeMillis) {
|
||||
double progress = (curTime - startTime) / (double) targetTimeMillis;
|
||||
updateProgress(progress, 1.0);
|
||||
|
||||
// 60fps would require 16msec sleep here.
|
||||
Uninterruptibles.sleepUninterruptibly(20, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
// Wait for the encryption thread before switching back to main UI.
|
||||
updateProgress(1.0, 1.0);
|
||||
aesKey = keyDerivationTask.get();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
checkGuiThread();
|
||||
onFinish(aesKey);
|
||||
}
|
||||
};
|
||||
progress = progressTask.progressProperty();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
new Thread(keyDerivationTask, "Key derivation").start();
|
||||
new Thread(progressTask, "Progress ticker").start();
|
||||
}
|
||||
|
||||
protected void onFinish(KeyParameter aesKey) {
|
||||
}
|
||||
}
|
@ -13,4 +13,4 @@
|
||||
|
||||
.root-pane {
|
||||
-fx-background-color: white;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.image.*?>
|
||||
<?import java.lang.*?>
|
||||
<?import java.util.*?>
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.effect.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.paint.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
|
||||
<StackPane maxHeight="Infinity" maxWidth="Infinity" prefHeight="400.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="wallettemplate.WalletPasswordController">
|
||||
<children>
|
||||
<AnchorPane maxHeight="400.0" maxWidth="600.0" styleClass="root-pane">
|
||||
<children>
|
||||
<ImageView fx:id="padlockImage" fitHeight="389.0" fitWidth="389.0" layoutX="14.0" layoutY="-4.0" opacity="0.22" pickOnBounds="true" preserveRatio="true" visible="false" AnchorPane.leftAnchor="14.0" AnchorPane.rightAnchor="197.0">
|
||||
<image>
|
||||
<!-- image from wikipedia -->
|
||||
<Image url="@200px-Padlock.svg.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
<HBox alignment="CENTER_LEFT" layoutX="26.0" prefHeight="68.0" prefWidth="600.0" styleClass="title-banner" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
|
||||
<children>
|
||||
<Label maxHeight="1.7976931348623157E308" styleClass="title-label" text="password">
|
||||
<font>
|
||||
<Font size="30.0" />
|
||||
</font>
|
||||
<HBox.margin>
|
||||
<Insets />
|
||||
</HBox.margin>
|
||||
<padding>
|
||||
<Insets left="20.0" right="20.0" />
|
||||
</padding>
|
||||
</Label>
|
||||
</children>
|
||||
</HBox>
|
||||
<ProgressIndicator fx:id="progressMeter" layoutX="218.0" layoutY="146.0" prefHeight="193.0" prefWidth="169.0" progress="0.0" AnchorPane.leftAnchor="218.0" AnchorPane.rightAnchor="213.0" />
|
||||
<Label fx:id="explanationLabel" layoutX="22.0" layoutY="83.0" prefHeight="52.0" prefWidth="561.0" text="Please enter your wallet password now:" wrapText="true" AnchorPane.leftAnchor="22.0" AnchorPane.rightAnchor="17.0">
|
||||
<font>
|
||||
<Font name="System Bold" size="13.0" />
|
||||
</font></Label>
|
||||
<HBox fx:id="buttonsBox" alignment="CENTER_RIGHT" layoutX="272.0" layoutY="360.0" prefHeight="26.0" prefWidth="561.0" spacing="20.0" AnchorPane.bottomAnchor="14.0" AnchorPane.rightAnchor="17.0">
|
||||
<children>
|
||||
<Button fx:id="cancelButton" cancelButton="true" mnemonicParsing="false" onAction="#cancelClicked" text="Cancel" />
|
||||
<Button fx:id="confirmButton" defaultButton="true" layoutX="523.0" layoutY="360.0" maxWidth="80.0" mnemonicParsing="false" onAction="#confirmClicked" text="Confirm" HBox.hgrow="ALWAYS" />
|
||||
</children>
|
||||
</HBox>
|
||||
<GridPane fx:id="widgetGrid" layoutX="22.0" layoutY="146.0" prefHeight="114.0" prefWidth="561.0" vgap="10.0" AnchorPane.leftAnchor="22.0" AnchorPane.rightAnchor="17.0">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="273.0" minWidth="10.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="417.0" minWidth="10.0" prefWidth="417.0" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints minHeight="10.0" vgrow="NEVER" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label text="Password" />
|
||||
<PasswordField fx:id="pass1" GridPane.columnIndex="1" />
|
||||
</children>
|
||||
</GridPane>
|
||||
</children>
|
||||
<effect>
|
||||
<DropShadow />
|
||||
</effect>
|
||||
</AnchorPane>
|
||||
</children>
|
||||
</StackPane>
|
@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.image.*?>
|
||||
<?import java.lang.*?>
|
||||
<?import java.util.*?>
|
||||
<?import javafx.geometry.*?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.effect.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import javafx.scene.paint.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
|
||||
<StackPane maxHeight="Infinity" maxWidth="Infinity" prefHeight="400.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="wallettemplate.WalletSetPasswordController">
|
||||
<children>
|
||||
<AnchorPane maxHeight="400.0" maxWidth="600.0" styleClass="root-pane">
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" layoutX="26.0" prefHeight="68.0" prefWidth="600.0" styleClass="title-banner" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
|
||||
<children>
|
||||
<Label maxHeight="1.7976931348623157E308" styleClass="title-label" text="password">
|
||||
<font>
|
||||
<Font size="30.0" />
|
||||
</font>
|
||||
<HBox.margin>
|
||||
<Insets />
|
||||
</HBox.margin>
|
||||
<padding>
|
||||
<Insets left="20.0" right="20.0" />
|
||||
</padding>
|
||||
</Label>
|
||||
</children>
|
||||
</HBox>
|
||||
<Label fx:id="explanationLabel" layoutX="22.0" layoutY="83.0" prefHeight="52.0" prefWidth="561.0" text="Setting a password on your wallet makes it safer against viruses and theft. You will need to enter your password whenever money is sent." wrapText="true" AnchorPane.leftAnchor="22.0" AnchorPane.rightAnchor="17.0" />
|
||||
<HBox alignment="CENTER_RIGHT" layoutX="272.0" layoutY="360.0" prefHeight="26.0" prefWidth="561.0" spacing="20.0" AnchorPane.bottomAnchor="14.0" AnchorPane.rightAnchor="17.0">
|
||||
<children>
|
||||
<Button fx:id="closeButton" defaultButton="true" layoutX="523.0" layoutY="360.0" maxWidth="80.0" mnemonicParsing="false" onAction="#closeClicked" text="Close" HBox.hgrow="ALWAYS" />
|
||||
</children>
|
||||
</HBox>
|
||||
<ProgressIndicator fx:id="progressMeter" layoutX="250.0" layoutY="133.0" prefHeight="114.0" prefWidth="87.0" progress="0.0" AnchorPane.leftAnchor="250.0" AnchorPane.rightAnchor="250.0" />
|
||||
<GridPane fx:id="widgetGrid" layoutX="22.0" layoutY="146.0" prefHeight="114.0" prefWidth="561.0" vgap="10.0" AnchorPane.leftAnchor="22.0" AnchorPane.rightAnchor="17.0">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="273.0" minWidth="10.0" />
|
||||
<ColumnConstraints hgrow="SOMETIMES" maxWidth="417.0" minWidth="10.0" prefWidth="417.0" />
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints minHeight="10.0" vgrow="NEVER" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="NEVER" />
|
||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
||||
</rowConstraints>
|
||||
<children>
|
||||
<Label text="Enter password" />
|
||||
<Label text="Repeat password" GridPane.rowIndex="1" />
|
||||
<PasswordField fx:id="pass1" GridPane.columnIndex="1" />
|
||||
<PasswordField fx:id="pass2" GridPane.columnIndex="1" GridPane.rowIndex="1" />
|
||||
<Button mnemonicParsing="false" onAction="#setPasswordClicked" text="Set password" GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="2" GridPane.valignment="TOP" />
|
||||
</children>
|
||||
</GridPane>
|
||||
<ImageView fx:id="padlockImage" fitHeight="125.0" fitWidth="125.0" layoutX="234.0" layoutY="262.0" pickOnBounds="true" preserveRatio="true" AnchorPane.leftAnchor="240.0" AnchorPane.rightAnchor="240.0">
|
||||
<image>
|
||||
<Image url="@200px-Padlock.svg.png" />
|
||||
</image>
|
||||
</ImageView>
|
||||
</children>
|
||||
<effect>
|
||||
<DropShadow />
|
||||
</effect>
|
||||
</AnchorPane>
|
||||
</children>
|
||||
</StackPane>
|
@ -9,48 +9,48 @@
|
||||
<?import javafx.scene.paint.*?>
|
||||
<?import javafx.scene.text.*?>
|
||||
|
||||
<StackPane maxHeight="Infinity" maxWidth="Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="wallettemplate.WalletSettingsController">
|
||||
<children>
|
||||
<AnchorPane maxHeight="400.0" maxWidth="600.0" styleClass="root-pane">
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" layoutX="26.0" prefHeight="68.0" prefWidth="600.0" styleClass="title-banner" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
|
||||
<children>
|
||||
<Label maxHeight="1.7976931348623157E308" styleClass="title-label" text="settings">
|
||||
<font>
|
||||
<Font size="30.0" />
|
||||
</font>
|
||||
<HBox.margin>
|
||||
<Insets />
|
||||
</HBox.margin>
|
||||
<padding>
|
||||
<Insets left="20.0" right="20.0" />
|
||||
</padding>
|
||||
</Label>
|
||||
</children>
|
||||
</HBox>
|
||||
<TextArea fx:id="wordsArea" layoutX="22.0" layoutY="155.0" prefHeight="127.0" prefWidth="561.0" styleClass="mnemonic-area" text="absorb tornado scrap blush purpose ethics destroy vicious abandon chunk labor inquiry" wrapText="true" AnchorPane.leftAnchor="22.0" AnchorPane.rightAnchor="17.0">
|
||||
<font>
|
||||
<Font size="30.0" />
|
||||
</font>
|
||||
</TextArea>
|
||||
<Label layoutX="35.0" layoutY="82.0" prefHeight="52.0" prefWidth="530.0" text="These are your wallet words. Write them down along with the creation date, and you can get your money back even if you lose all your wallet backup files. Just type the details back in below to restore!" textAlignment="CENTER" wrapText="true" />
|
||||
<HBox alignment="CENTER_RIGHT" layoutX="22.0" layoutY="292.0" prefHeight="26.0" prefWidth="561.0" spacing="10.0" AnchorPane.rightAnchor="17.0">
|
||||
<children>
|
||||
<Label layoutX="64.0" layoutY="283.0" text="Created on:" />
|
||||
<DatePicker fx:id="datePicker" />
|
||||
</children>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_RIGHT" layoutX="272.0" layoutY="360.0" prefHeight="26.0" prefWidth="311.0" spacing="20.0" AnchorPane.bottomAnchor="14.0" AnchorPane.rightAnchor="17.0">
|
||||
<children>
|
||||
<Button maxWidth="80.0" mnemonicParsing="false" text="Backup" HBox.hgrow="ALWAYS" />
|
||||
<Button fx:id="restoreButton" maxWidth="80.0" mnemonicParsing="false" onAction="#restoreClicked" text="Restore" HBox.hgrow="ALWAYS" />
|
||||
<Button defaultButton="true" layoutX="523.0" layoutY="360.0" maxWidth="80.0" mnemonicParsing="false" onAction="#closeClicked" text="Close" HBox.hgrow="ALWAYS" />
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
<effect>
|
||||
<DropShadow />
|
||||
</effect>
|
||||
</AnchorPane>
|
||||
</children>
|
||||
<StackPane maxHeight="Infinity" maxWidth="Infinity" prefHeight="400.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="wallettemplate.WalletSettingsController">
|
||||
<children>
|
||||
<AnchorPane maxHeight="400.0" maxWidth="600.0" styleClass="root-pane">
|
||||
<children>
|
||||
<HBox alignment="CENTER_LEFT" layoutX="26.0" prefHeight="68.0" prefWidth="600.0" styleClass="title-banner" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0">
|
||||
<children>
|
||||
<Label maxHeight="1.7976931348623157E308" styleClass="title-label" text="settings">
|
||||
<font>
|
||||
<Font size="30.0" />
|
||||
</font>
|
||||
<HBox.margin>
|
||||
<Insets />
|
||||
</HBox.margin>
|
||||
<padding>
|
||||
<Insets left="20.0" right="20.0" />
|
||||
</padding>
|
||||
</Label>
|
||||
</children>
|
||||
</HBox>
|
||||
<TextArea fx:id="wordsArea" layoutX="22.0" layoutY="155.0" prefHeight="127.0" prefWidth="561.0" styleClass="mnemonic-area" text="absorb tornado scrap blush purpose ethics destroy vicious abandon chunk labor inquiry" wrapText="true" AnchorPane.leftAnchor="22.0" AnchorPane.rightAnchor="17.0">
|
||||
<font>
|
||||
<Font size="30.0" />
|
||||
</font>
|
||||
</TextArea>
|
||||
<Label layoutX="22.0" layoutY="83.0" prefHeight="52.0" prefWidth="561.0" text="These are your wallet words. Write them down along with the creation date, and you can get your money back even if you lose all your wallet backup files. Just type the details back in below to restore!" wrapText="true" />
|
||||
<HBox alignment="CENTER_RIGHT" layoutX="22.0" layoutY="292.0" prefHeight="26.0" prefWidth="561.0" spacing="10.0" AnchorPane.rightAnchor="17.0">
|
||||
<children>
|
||||
<Label layoutX="64.0" layoutY="283.0" text="Created on:" />
|
||||
<DatePicker fx:id="datePicker" />
|
||||
</children>
|
||||
</HBox>
|
||||
<HBox alignment="CENTER_RIGHT" layoutX="272.0" layoutY="360.0" prefHeight="26.0" prefWidth="561.0" spacing="20.0" AnchorPane.bottomAnchor="14.0" AnchorPane.rightAnchor="17.0">
|
||||
<children>
|
||||
<Button fx:id="passwordButton" mnemonicParsing="false" onAction="#passwordButtonClicked" text="Set password" />
|
||||
<Button fx:id="restoreButton" mnemonicParsing="false" onAction="#restoreClicked" text="Restore from words" HBox.hgrow="ALWAYS" />
|
||||
<Button defaultButton="true" layoutX="523.0" layoutY="360.0" maxWidth="80.0" mnemonicParsing="false" onAction="#closeClicked" text="Close" HBox.hgrow="ALWAYS" />
|
||||
</children>
|
||||
</HBox>
|
||||
</children>
|
||||
<effect>
|
||||
<DropShadow />
|
||||
</effect>
|
||||
</AnchorPane>
|
||||
</children>
|
||||
</StackPane>
|
||||
|
Loading…
Reference in New Issue
Block a user