walletfx: Extract WalletApplication from WalletTemplate

* Extract abstract class `WalletApplication` from `WalletTemplate`
* `WalletTemplate` implements `loadController()` with resource names
* `MainController`: Add `scene` member and `scene()` method
This commit is contained in:
Sean Gilligan 2021-09-22 11:34:32 -07:00
parent d8b6733c9c
commit d1aa5e3d36
7 changed files with 187 additions and 141 deletions

View file

@ -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);
}
}

View file

@ -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() {

View file

@ -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<SendMoneyControlle
// Called by FXMLLoader
public void initialize() {
Coin balance = WalletTemplate.bitcoin.wallet().getBalance();
Coin balance = WalletApplication.bitcoin.wallet().getBalance();
checkState(!balance.isZero());
new BitcoinAddressValidator(WalletTemplate.instance.params, address, sendBtn);
new BitcoinAddressValidator(WalletApplication.instance.params, address, sendBtn);
new TextFieldValidator(amountEdit, text ->
!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<SendMoneyControlle
// Address exception cannot happen as we validated it beforehand.
try {
Coin amount = Coin.parseCoin(amountEdit.getText());
Address destination = Address.fromString(WalletTemplate.instance.params, address.getText());
Address destination = Address.fromString(WalletApplication.instance.params, address.getText());
SendRequest req;
if (amount.equals(WalletTemplate.bitcoin.wallet().getBalance()))
if (amount.equals(WalletApplication.bitcoin.wallet().getBalance()))
req = SendRequest.emptyWallet(destination);
else
req = SendRequest.to(destination, amount);
@ -90,7 +91,7 @@ public class SendMoneyController implements OverlayController<SendMoneyControlle
// Don't make the user wait for confirmations for now, as the intention is they're sending it
// their own money!
req.allowUnconfirmed();
sendResult = WalletTemplate.bitcoin.wallet().sendCoins(req);
sendResult = WalletApplication.bitcoin.wallet().sendCoins(req);
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Transaction result) {

View file

@ -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<WalletPasswor
return;
}
final KeyCrypterScrypt keyCrypter = (KeyCrypterScrypt) WalletTemplate.bitcoin.wallet().getKeyCrypter();
final KeyCrypterScrypt keyCrypter = (KeyCrypterScrypt) WalletApplication.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 final void onFinish(KeyParameter aesKey, int timeTakenMsec) {
checkGuiThread();
if (WalletTemplate.bitcoin.wallet().checkAESKey(aesKey)) {
if (WalletApplication.bitcoin.wallet().checkAESKey(aesKey)) {
WalletPasswordController.this.aesKey.set(aesKey);
} else {
log.warn("User entered incorrect password");
@ -122,11 +123,11 @@ public class WalletPasswordController implements OverlayController<WalletPasswor
// 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()));
WalletTemplate.bitcoin.wallet().setTag(TAG, bytes);
WalletApplication.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(WalletTemplate.bitcoin.wallet().getTag(TAG).toByteArray()));
return Duration.ofMillis(Longs.fromByteArray(WalletApplication.bitcoin.wallet().getTag(TAG).toByteArray()));
}
}

View file

@ -23,6 +23,7 @@ import javafx.scene.control.*;
import javafx.scene.layout.*;
import org.bitcoinj.crypto.*;
import org.bitcoinj.wallet.*;
import org.bitcoinj.walletfx.application.WalletApplication;
import org.bitcoinj.walletfx.overlay.OverlayController;
import org.bitcoinj.walletfx.overlay.OverlayableStackPaneController;
import org.slf4j.*;
@ -117,7 +118,7 @@ public class WalletSetPasswordController implements OverlayController<WalletSetP
WalletPasswordController.setTargetTime(Duration.ofMillis(timeTakenMsec));
// The actual encryption part doesn't take very long as most private keys are derived on demand.
log.info("Key derived, now encrypting");
WalletTemplate.bitcoin.wallet().encrypt(scrypt, aesKey);
WalletApplication.bitcoin.wallet().encrypt(scrypt, aesKey);
log.info("Encryption done");
informationalAlert("Wallet encrypted",
"You can remove the password at any time from the settings screen.");

View file

@ -28,6 +28,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TextArea;
import org.bitcoinj.walletfx.application.WalletApplication;
import org.bitcoinj.walletfx.overlay.OverlayController;
import org.bitcoinj.walletfx.overlay.OverlayableStackPaneController;
import org.slf4j.Logger;
@ -70,7 +71,7 @@ public class WalletSettingsController implements OverlayController<WalletSetting
// Note: NOT called by FXMLLoader!
public void initialize(@Nullable KeyParameter aesKey) {
DeterministicSeed seed = WalletTemplate.bitcoin.wallet().getKeyChainSeed();
DeterministicSeed seed = WalletApplication.bitcoin.wallet().getKeyChainSeed();
if (aesKey == null) {
if (seed.isEncrypted()) {
log.info("Wallet is encrypted, requesting password first.");
@ -80,7 +81,7 @@ public class WalletSettingsController implements OverlayController<WalletSetting
}
} else {
this.aesKey = aesKey;
seed = seed.decrypt(checkNotNull(WalletTemplate.bitcoin.wallet().getKeyCrypter()), "", aesKey);
seed = seed.decrypt(checkNotNull(WalletApplication.bitcoin.wallet().getKeyCrypter()), "", aesKey);
// Now we can display the wallet seed as appropriate.
passwordButton.setText("Remove password");
}
@ -158,7 +159,7 @@ public class WalletSettingsController implements OverlayController<WalletSetting
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 (WalletTemplate.bitcoin.wallet().getBalance().value > 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<WalletSetting
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.
WalletTemplate.bitcoin.addListener(new Service.Listener() {
WalletApplication.bitcoin.addListener(new Service.Listener() {
@Override
public void terminated(Service.State from) {
WalletTemplate.instance.setupWalletKit(seed);
WalletTemplate.bitcoin.startAsync();
WalletApplication.instance.setupWalletKit(seed);
WalletApplication.bitcoin.startAsync();
}
}, Platform::runLater);
WalletTemplate.bitcoin.stopAsync();
WalletApplication.bitcoin.stopAsync();
}
@ -195,7 +196,7 @@ public class WalletSettingsController implements OverlayController<WalletSetting
if (aesKey == null) {
rootController.overlayUI("wallet_set_password.fxml");
} else {
WalletTemplate.bitcoin.wallet().decrypt(aesKey);
WalletApplication.bitcoin.wallet().decrypt(aesKey);
informationalAlert("Wallet decrypted", "A password will no longer be required to send money or edit settings.");
passwordButton.setText("Set password");
aesKey = null;

View file

@ -16,142 +16,33 @@
package wallettemplate;
import com.google.common.util.concurrent.Service;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.Pane;
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.application.AppDelegate;
import org.bitcoinj.walletfx.utils.GuiUtils;
import org.bitcoinj.walletfx.application.WalletApplication;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import static org.bitcoinj.walletfx.utils.GuiUtils.informationalAlert;
/**
*
* Template implementation of WalletApplication
*/
public class WalletTemplate implements AppDelegate {
static WalletAppKit bitcoin;
static WalletTemplate instance;
public final String applicationName;
private final String walletFileName;
public final NetworkParameters params;
public final Script.ScriptType preferredOutputScriptType;
private MainController controller;
public class WalletTemplate extends WalletApplication {
public WalletTemplate(String applicationName, NetworkParameters params, Script.ScriptType preferredOutputScriptType) {
instance = this;
this.applicationName = applicationName;
this.params = params;
this.preferredOutputScriptType = preferredOutputScriptType;
this.walletFileName = applicationName.replaceAll("[^a-zA-Z0-9.-]", "_") + "-" + params.getPaymentProtocolId();
super(applicationName, params, preferredOutputScriptType);
}
@Override
public void start(Stage mainWindow) throws Exception {
try {
realStart(mainWindow);
} catch (Throwable e) {
GuiUtils.crashAlert(e);
throw e;
}
}
private void realStart(Stage mainWindow) throws IOException {
instance = this;
// 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();
}
protected MainController loadController() throws IOException {
// Load the GUI. The MainController class will be automagically created and wired up.
URL location = getClass().getResource("main.fxml");
FXMLLoader loader = new FXMLLoader(location);
Pane mainUI = loader.load();
controller = loader.getController();
MainController controller = loader.getController();
Scene scene = controller.controllerStart(mainUI, "wallet.css");
mainWindow.setScene(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();
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);
controller.controllerStart(mainUI, "wallet.css");
return controller;
}
}