From 05877abff78a517ffb4d519e87dfa0127a2ff331 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Fri, 11 Jul 2014 00:27:00 +0200 Subject: [PATCH] Wallet template: add a UI to show the users seed words and demonstrate how to do a restore from seed with WalletAppKit. --- wallettemplate/pom.xml | 2 + .../main/java/wallettemplate/Controller.java | 18 +++ .../src/main/java/wallettemplate/Main.java | 40 ++++-- .../WalletSettingsController.java | 128 ++++++++++++++++++ .../utils/TextFieldValidator.java | 22 +-- .../java/wallettemplate/utils/WTUtils.java | 70 ++++++++++ .../main/resources/wallettemplate/main.fxml | 9 +- .../resources/wallettemplate/utils/alert.fxml | 2 +- .../wallettemplate/utils/text-validation.css | 22 ++- .../main/resources/wallettemplate/wallet.css | 16 +++ .../wallettemplate/wallet_settings.fxml | 56 ++++++++ 11 files changed, 357 insertions(+), 28 deletions(-) create mode 100644 wallettemplate/src/main/java/wallettemplate/WalletSettingsController.java create mode 100644 wallettemplate/src/main/java/wallettemplate/utils/WTUtils.java create mode 100644 wallettemplate/src/main/resources/wallettemplate/wallet.css create mode 100644 wallettemplate/src/main/resources/wallettemplate/wallet_settings.fxml diff --git a/wallettemplate/pom.xml b/wallettemplate/pom.xml index 8e1f7826..4f4b610f 100644 --- a/wallettemplate/pom.xml +++ b/wallettemplate/pom.xml @@ -39,11 +39,13 @@ guava 16.0.1 + de.jensd fontawesomefx diff --git a/wallettemplate/src/main/java/wallettemplate/Controller.java b/wallettemplate/src/main/java/wallettemplate/Controller.java index 715bc618..9624b667 100644 --- a/wallettemplate/src/main/java/wallettemplate/Controller.java +++ b/wallettemplate/src/main/java/wallettemplate/Controller.java @@ -46,6 +46,10 @@ public class Controller { Main.instance.overlayUI("send_money.fxml"); } + public void settingsClicked(ActionEvent event) { + Main.instance.overlayUI("wallet_settings.fxml"); + } + public class ProgressBarUpdater extends DownloadListener { @Override protected void progress(double pct, int blocksSoFar, Date date) { @@ -60,6 +64,20 @@ public class Controller { } } + public void restoreFromSeedAnimation() { + // Buttons slide out ... + TranslateTransition leave = new TranslateTransition(Duration.millis(600), controlsBox); + leave.setByY(80.0); + // Sync bar slides in ... + TranslateTransition arrive = new TranslateTransition(Duration.millis(600), syncBox); + arrive.setToY(0.0); + // Slide out happens then slide in/fade happens. + SequentialTransition both = new SequentialTransition(leave, arrive); + both.setCycleCount(1); + both.setInterpolator(Interpolator.EASE_BOTH); + both.play(); + } + public void readyToGoAnimation() { // Sync progress bar slides out ... TranslateTransition leave = new TranslateTransition(Duration.millis(600), syncBox); diff --git a/wallettemplate/src/main/java/wallettemplate/Main.java b/wallettemplate/src/main/java/wallettemplate/Main.java index c3183193..121ae84a 100644 --- a/wallettemplate/src/main/java/wallettemplate/Main.java +++ b/wallettemplate/src/main/java/wallettemplate/Main.java @@ -1,12 +1,12 @@ package wallettemplate; -import com.aquafx_project.AquaFx; import com.google.bitcoin.core.NetworkParameters; import com.google.bitcoin.kits.WalletAppKit; import com.google.bitcoin.params.MainNetParams; import com.google.bitcoin.params.RegTestParams; import com.google.bitcoin.utils.BriefLogFormatter; import com.google.bitcoin.utils.Threading; +import com.google.bitcoin.wallet.DeterministicSeed; import javafx.application.Application; import javafx.application.Platform; import javafx.fxml.FXMLLoader; @@ -18,6 +18,7 @@ import javafx.stage.Stage; import wallettemplate.utils.GuiUtils; import wallettemplate.utils.TextFieldValidator; +import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.net.URL; @@ -33,6 +34,7 @@ public class Main extends Application { private StackPane uiStack; private Pane mainUI; + public Controller controller; @Override public void start(Stage mainWindow) throws Exception { @@ -40,21 +42,23 @@ public class Main extends Application { // Show the crash dialog for any exceptions that we don't handle and that hit the main loop. GuiUtils.handleCrashesOnThisThread(); - // Match Aqua UI style. if (System.getProperty("os.name").toLowerCase().contains("mac")) { - AquaFx.style(); + // We could match the Mac Aqua style here, except that (a) Modena doesn't look that bad, and (b) + // the date picker widget is kinda broken in AquaFx and I can't be bothered fixing it. + // AquaFx.style(); } // Load the GUI. The Controller class will be automagically created and wired up. URL location = getClass().getResource("main.fxml"); FXMLLoader loader = new FXMLLoader(location); mainUI = loader.load(); - Controller controller = loader.getController(); + controller = loader.getController(); // Configure the window with a StackPane so we can overlay things on top of the main UI. uiStack = new StackPane(mainUI); mainWindow.setTitle(APP_NAME); final Scene scene = new Scene(uiStack); TextFieldValidator.configureScene(scene); // Add CSS that we need. + scene.getStylesheets().add(getClass().getResource("wallet.css").toString()); mainWindow.setScene(scene); // Make log output concise. @@ -65,7 +69,8 @@ public class Main extends Application { // a future version. Threading.USER_THREAD = Platform::runLater; // Create the app kit. It won't do any heavyweight initialization until after we start it. - bitcoin = new WalletAppKit(params, new File("."), APP_NAME); + setupWalletKit(null); + if (bitcoin.isChainFileLocked()) { informationalAlert("Already running", "This application is already running and cannot be started twice."); Platform.exit(); @@ -74,6 +79,22 @@ public class Main extends Application { mainWindow.show(); + bitcoin.startAsync(); + } + + public void setupWalletKit(@Nullable DeterministicSeed seed) { + // If seed is non-null it means we are restoring from backup. + bitcoin = new WalletAppKit(params, new File("."), APP_NAME) { + @Override + protected void onSetupCompleted() { + // 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); + bitcoin.peerGroup().setBloomFilterFalsePositiveRate(0.00001); + Platform.runLater(controller::onBitcoinSetup); + } + }; // Now configure and start the appkit. This will take a second or two - we could show a temporary splash screen // or progress widget to keep the user engaged whilst we initialise, but we don't. if (params == RegTestParams.get()) { @@ -90,13 +111,8 @@ public class Main extends Application { bitcoin.setDownloadListener(controller.progressBarUpdater()) .setBlockingStartup(false) .setUserAgent(APP_NAME, "1.0"); - bitcoin.startAsync(); - bitcoin.awaitRunning(); - // 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); - System.out.println(bitcoin.wallet()); - controller.onBitcoinSetup(); + if (seed != null) + bitcoin.restoreWalletFromSeed(seed); } public class OverlayUI { diff --git a/wallettemplate/src/main/java/wallettemplate/WalletSettingsController.java b/wallettemplate/src/main/java/wallettemplate/WalletSettingsController.java new file mode 100644 index 00000000..abe27b99 --- /dev/null +++ b/wallettemplate/src/main/java/wallettemplate/WalletSettingsController.java @@ -0,0 +1,128 @@ +package wallettemplate; + +import com.google.bitcoin.crypto.MnemonicCode; +import com.google.bitcoin.wallet.DeterministicSeed; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.util.concurrent.Service; +import javafx.application.Platform; +import javafx.beans.binding.BooleanBinding; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.DatePicker; +import javafx.scene.control.TextArea; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import wallettemplate.utils.TextFieldValidator; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.ZoneOffset; + +import static javafx.beans.binding.Bindings.*; +import static wallettemplate.utils.GuiUtils.informationalAlert; +import static wallettemplate.utils.WTUtils.didThrow; +import static wallettemplate.utils.WTUtils.unchecked; + +public class WalletSettingsController { + private static final Logger log = LoggerFactory.getLogger(WalletSettingsController.class); + + @FXML DatePicker datePicker; + @FXML TextArea wordsArea; + @FXML Button restoreButton; + + public Main.OverlayUI overlayUi; + + // Called by FXMLLoader + public void initialize() { + final DeterministicSeed seed = Main.bitcoin.wallet().getKeyChainSeed(); + + // Set the date picker to show the birthday of this wallet. + Instant creationTime = Instant.ofEpochSecond(seed.getCreationTimeSeconds()); + final LocalDate origDate = creationTime.atZone(ZoneId.systemDefault()).toLocalDate(); + datePicker.setValue(origDate); + + // Set the mnemonic seed words. + final String origWords = Joiner.on(" ").join(seed.getMnemonicCode()); + 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"); + } + }); + } + + public void closeClicked(ActionEvent event) { + overlayUi.done(); + } + + 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().value > 0) { + informationalAlert("Wallet is not empty", + "You must empty this wallet out before attempting to restore an older one, as mixing wallets " + + "together can lead to invalidated backups."); + return; + } + + 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(); + Main.instance.controller.restoreFromSeedAnimation(); + + long birthday = datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC); + DeterministicSeed seed = new DeterministicSeed(Splitter.on(' ').splitToList(wordsArea.getText()), "", birthday); + // Shut down bitcoinj and restart it with the new seed. + Main.bitcoin.addListener(new Service.Listener() { + @Override + public void terminated(Service.State from) { + super.terminated(from); + Main.instance.setupWalletKit(seed); + Main.bitcoin.startAsync(); + } + }, Platform::runLater); + Main.bitcoin.stopAsync(); + } +} diff --git a/wallettemplate/src/main/java/wallettemplate/utils/TextFieldValidator.java b/wallettemplate/src/main/java/wallettemplate/utils/TextFieldValidator.java index 9e484ddc..28798c4b 100644 --- a/wallettemplate/src/main/java/wallettemplate/utils/TextFieldValidator.java +++ b/wallettemplate/src/main/java/wallettemplate/utils/TextFieldValidator.java @@ -1,25 +1,27 @@ package wallettemplate.utils; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.Scene; -import javafx.scene.control.TextField; +import javafx.scene.control.TextInputControl; import java.util.function.Predicate; public class TextFieldValidator { - private boolean valid; + public final BooleanProperty valid = new SimpleBooleanProperty(false); - public TextFieldValidator(TextField textField, Predicate validator) { - this.valid = validator.test(textField.getText()); - apply(textField, valid); - textField.textProperty().addListener((observableValue, prev, current) -> { + public TextFieldValidator(TextInputControl control, Predicate validator) { + this.valid.set(validator.test(control.getText())); + apply(control, valid.get()); + control.textProperty().addListener((observableValue, prev, current) -> { boolean nowValid = validator.test(current); - if (nowValid == valid) return; - apply(textField, nowValid); - valid = nowValid; + if (nowValid == valid.get()) return; + valid.set(nowValid); }); + valid.addListener(o -> apply(control, valid.get())); } - private static void apply(TextField textField, boolean nowValid) { + private static void apply(TextInputControl textField, boolean nowValid) { if (nowValid) { textField.getStyleClass().remove("validation_error"); } else { diff --git a/wallettemplate/src/main/java/wallettemplate/utils/WTUtils.java b/wallettemplate/src/main/java/wallettemplate/utils/WTUtils.java new file mode 100644 index 00000000..3f747704 --- /dev/null +++ b/wallettemplate/src/main/java/wallettemplate/utils/WTUtils.java @@ -0,0 +1,70 @@ +package wallettemplate.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Some generic utilities to make Java a bit less annoying. + */ +public class WTUtils { + private static final Logger log = LoggerFactory.getLogger(WTUtils.class); + + public interface UncheckedRun { + public T run() throws Throwable; + } + + public interface UncheckedRunnable { + public void run() throws Throwable; + } + + public static T unchecked(UncheckedRun run) { + try { + return run.run(); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + + public static void uncheck(UncheckedRunnable run) { + try { + run.run(); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + + public static void ignoreAndLog(UncheckedRunnable runnable) { + try { + runnable.run(); + } catch (Throwable t) { + log.error("Ignoring error", t); + } + } + + public static T ignoredAndLogged(UncheckedRun runnable) { + try { + return runnable.run(); + } catch (Throwable t) { + log.error("Ignoring error", t); + return null; + } + } + + public static boolean didThrow(UncheckedRun run) { + try { + run.run(); + return false; + } catch (Throwable throwable) { + return true; + } + } + + public static boolean didThrow(UncheckedRunnable run) { + try { + run.run(); + return false; + } catch (Throwable throwable) { + return true; + } + } +} diff --git a/wallettemplate/src/main/resources/wallettemplate/main.fxml b/wallettemplate/src/main/resources/wallettemplate/main.fxml index 28ce9303..a8ceb2e3 100644 --- a/wallettemplate/src/main/resources/wallettemplate/main.fxml +++ b/wallettemplate/src/main/resources/wallettemplate/main.fxml @@ -15,7 +15,7 @@ - +