diff --git a/wallettemplate/src/main/java/org/bitcoinj/walletfx/application/WalletApplication.java b/wallettemplate/src/main/java/org/bitcoinj/walletfx/application/WalletApplication.java new file mode 100644 index 000000000..62327d222 --- /dev/null +++ b/wallettemplate/src/main/java/org/bitcoinj/walletfx/application/WalletApplication.java @@ -0,0 +1,146 @@ +/* + * Copyright by the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bitcoinj.walletfx.application; + +import com.google.common.util.concurrent.Service; +import javafx.application.Platform; +import javafx.scene.input.KeyCombination; +import javafx.stage.Stage; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Utils; +import org.bitcoinj.kits.WalletAppKit; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.script.Script; +import org.bitcoinj.utils.AppDataDirectory; +import org.bitcoinj.utils.BriefLogFormatter; +import org.bitcoinj.utils.Threading; +import org.bitcoinj.wallet.DeterministicSeed; +import org.bitcoinj.walletfx.utils.GuiUtils; +import wallettemplate.MainController; +import wallettemplate.WalletSetPasswordController; + +import javax.annotation.Nullable; +import java.io.File; +import java.io.IOException; + +import static org.bitcoinj.walletfx.utils.GuiUtils.informationalAlert; + +/** + * Base class for JavaFX Wallet Applications + */ +public abstract class WalletApplication implements AppDelegate { + public static WalletAppKit bitcoin; + public static WalletApplication instance; + public final String applicationName; + public final NetworkParameters params; + public final Script.ScriptType preferredOutputScriptType; + protected final String walletFileName; + private MainController controller; + + public WalletApplication(String applicationName, NetworkParameters params, Script.ScriptType preferredOutputScriptType) { + instance = this; + this.applicationName = applicationName; + this.walletFileName = applicationName.replaceAll("[^a-zA-Z0-9.-]", "_") + "-" + params.getPaymentProtocolId(); + this.params = params; + this.preferredOutputScriptType = preferredOutputScriptType; + } + + @Override + public void start(Stage mainWindow) throws Exception { + try { + realStart(mainWindow); + } catch (Throwable e) { + GuiUtils.crashAlert(e); + throw e; + } + } + + abstract protected MainController loadController() throws IOException; + + private void realStart(Stage mainWindow) throws IOException { + // Show the crash dialog for any exceptions that we don't handle and that hit the main loop. + GuiUtils.handleCrashesOnThisThread(); + + if (Utils.isMac()) { + // 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(); + } + + controller = loadController(); + mainWindow.setScene(controller.scene()); + + // Make log output concise. + BriefLogFormatter.init(); + // Tell bitcoinj to run event handlers on the JavaFX UI thread. This keeps things simple and means + // we cannot forget to switch threads when adding event handlers. Unfortunately, the DownloadListener + // we give to the app kit is currently an exception and runs on a library thread. It'll get fixed in + // a future version. + Threading.USER_THREAD = Platform::runLater; + // Create the app kit. It won't do any heavyweight initialization until after we start it. + setupWalletKit(null); + + if (bitcoin.isChainFileLocked()) { + informationalAlert("Already running", "This application is already running and cannot be started twice."); + Platform.exit(); + return; + } + + mainWindow.show(); + + WalletSetPasswordController.estimateKeyDerivationTimeMsec(); + + bitcoin.addListener(new Service.Listener() { + @Override + public void failed(Service.State from, Throwable failure) { + GuiUtils.crashAlert(failure); + } + }, Platform::runLater); + bitcoin.startAsync(); + + controller.scene().getAccelerators().put(KeyCombination.valueOf("Shortcut+F"), () -> bitcoin.peerGroup().getDownloadPeer().close()); + } + + public void setupWalletKit(@Nullable DeterministicSeed seed) { + // If seed is non-null it means we are restoring from backup. + File appDataDirectory = AppDataDirectory.get(applicationName).toFile(); + bitcoin = new WalletAppKit(params, preferredOutputScriptType, null, appDataDirectory, walletFileName) { + @Override + protected void onSetupCompleted() { + 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()) { + bitcoin.connectToLocalHost(); // You should run a regtest mode bitcoind locally. + } + bitcoin.setDownloadListener(controller.progressBarUpdater()) + .setBlockingStartup(false) + .setUserAgent(applicationName, "1.0"); + if (seed != null) + bitcoin.restoreWalletFromSeed(seed); + } + + @Override + public void stop() throws Exception { + bitcoin.stopAsync(); + bitcoin.awaitTerminated(); + // Forcibly terminate the JVM because Orchid likes to spew non-daemon threads everywhere. + Runtime.getRuntime().exit(0); + } +} diff --git a/wallettemplate/src/main/java/wallettemplate/MainController.java b/wallettemplate/src/main/java/wallettemplate/MainController.java index 1bae1d15e..1e96bab95 100644 --- a/wallettemplate/src/main/java/wallettemplate/MainController.java +++ b/wallettemplate/src/main/java/wallettemplate/MainController.java @@ -33,6 +33,7 @@ import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.HBox; import javafx.util.Duration; +import org.bitcoinj.walletfx.application.WalletApplication; import org.bitcoinj.walletfx.overlay.OverlayableStackPaneController; import org.bitcoinj.walletfx.utils.GuiUtils; import org.bitcoinj.walletfx.utils.TextFieldValidator; @@ -42,7 +43,7 @@ import org.bitcoinj.walletfx.utils.BitcoinUIModel; import org.bitcoinj.walletfx.utils.easing.EasingMode; import org.bitcoinj.walletfx.utils.easing.ElasticInterpolator; -import static wallettemplate.WalletTemplate.bitcoin; +import static org.bitcoinj.walletfx.application.WalletApplication.bitcoin; /** * Gets created auto-magically by FXMLLoader via reflection. The widget fields are set to the GUI controls they're named @@ -59,6 +60,7 @@ public class MainController extends OverlayableStackPaneController { private NotificationBarPane.Item syncItem; private static final MonetaryFormat MONETARY_FORMAT = MonetaryFormat.BTC.noCode(); + private Scene scene; private NotificationBarPane notificationBar; // Called by FXMLLoader. @@ -67,24 +69,27 @@ public class MainController extends OverlayableStackPaneController { // Special case of initOverlay that passes null as the 2nd parameter because ClickableBitcoinAddress is loaded by FXML // TODO: Extract QRCode Pane to separate reusable class that is a more standard OverlayController instance addressControl.initOverlay(this, null); - addressControl.setAppName(WalletTemplate.instance.applicationName); + addressControl.setAppName(WalletApplication.instance.applicationName); addressControl.setOpacity(0.0); } - Scene controllerStart(Pane mainUI, String cssResourceName) { + public Scene scene() { + return scene; + } + + public void controllerStart(Pane mainUI, String cssResourceName) { this.mainUI = mainUI; // Configure the window with a StackPane so we can overlay things on top of the main UI, and a // NotificationBarPane so we can slide messages and progress bars in from the bottom. Note that // ordering of the construction and connection matters here, otherwise we get (harmless) CSS error // spew to the logs. notificationBar = new NotificationBarPane(mainUI); - Scene scene = new Scene(uiStack); + scene = new Scene(uiStack); TextFieldValidator.configureScene(scene); // Add CSS that we need. cssResourceName will be loaded from the same package as this class. scene.getStylesheets().add(getClass().getResource(cssResourceName).toString()); uiStack.getChildren().add(notificationBar); scene.getAccelerators().put(KeyCombination.valueOf("Shortcut+F"), () -> bitcoin.peerGroup().getDownloadPeer().close()); - return scene; } public void onBitcoinSetup() { diff --git a/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java b/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java index 752ee103e..a9b029162 100644 --- a/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java +++ b/wallettemplate/src/main/java/wallettemplate/SendMoneyController.java @@ -29,6 +29,7 @@ import javafx.event.ActionEvent; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TextField; +import org.bitcoinj.walletfx.application.WalletApplication; import org.bitcoinj.walletfx.overlay.OverlayController; import org.bitcoinj.walletfx.overlay.OverlayableStackPaneController; import org.bouncycastle.crypto.params.KeyParameter; @@ -63,13 +64,13 @@ public class SendMoneyController implements OverlayController !WTUtils.didThrow(() -> checkState(Coin.parseCoin(text).compareTo(balance) <= 0))); amountEdit.setText(balance.toPlainString()); - address.setPromptText(Address.fromKey(WalletTemplate.instance.params, new ECKey(), WalletTemplate.instance.preferredOutputScriptType).toString()); + address.setPromptText(Address.fromKey(WalletApplication.instance.params, new ECKey(), WalletApplication.instance.preferredOutputScriptType).toString()); } public void cancel(ActionEvent event) { @@ -80,9 +81,9 @@ public class SendMoneyController implements OverlayController() { @Override public void onSuccess(@Nullable Transaction result) { diff --git a/wallettemplate/src/main/java/wallettemplate/WalletPasswordController.java b/wallettemplate/src/main/java/wallettemplate/WalletPasswordController.java index 2af8a2caa..37b534b7b 100644 --- a/wallettemplate/src/main/java/wallettemplate/WalletPasswordController.java +++ b/wallettemplate/src/main/java/wallettemplate/WalletPasswordController.java @@ -30,6 +30,7 @@ import javafx.scene.control.ProgressIndicator; import javafx.scene.image.ImageView; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; +import org.bitcoinj.walletfx.application.WalletApplication; import org.bitcoinj.walletfx.overlay.OverlayController; import org.bitcoinj.walletfx.overlay.OverlayableStackPaneController; import org.slf4j.Logger; @@ -79,13 +80,13 @@ public class WalletPasswordController implements OverlayController 0) { + if (WalletApplication.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."); @@ -180,14 +181,14 @@ public class WalletSettingsController implements OverlayController bitcoin.peerGroup().getDownloadPeer().close()); - } - - public void setupWalletKit(@Nullable DeterministicSeed seed) { - // If seed is non-null it means we are restoring from backup. - File appDataDirectory = AppDataDirectory.get(applicationName).toFile(); - bitcoin = new WalletAppKit(params, preferredOutputScriptType, null, appDataDirectory, walletFileName) { - @Override - protected void onSetupCompleted() { - 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()) { - bitcoin.connectToLocalHost(); // You should run a regtest mode bitcoind locally. - } - bitcoin.setDownloadListener(controller.progressBarUpdater()) - .setBlockingStartup(false) - .setUserAgent(applicationName, "1.0"); - if (seed != null) - bitcoin.restoreWalletFromSeed(seed); - } - - - @Override - public void stop() throws Exception { - bitcoin.stopAsync(); - bitcoin.awaitTerminated(); - // Forcibly terminate the JVM because Orchid likes to spew non-daemon threads everywhere. - Runtime.getRuntime().exit(0); + controller.controllerStart(mainUI, "wallet.css"); + return controller; } }