walletfx: Create WalletTemplate implementing AppDelegate

* Introduce `AppDelegate` class for delegating JavaFX `Application`
* Move almost all of `Main` to `WalletTemplate`

Rationale:

* The “template” JavaFX Application main class (`Main`) is now about 30 lines of code
* `Main` class allows easy switching between TestNet and MainNet (in fact it could become a command-line argument) and other configuration changes (e.g. `preferredOutputScriptType`)
* Prepares the way for the next steps of refactoring
This commit is contained in:
Sean Gilligan 2021-09-22 09:17:40 -07:00
parent a7161eed8e
commit d8b6733c9c
8 changed files with 245 additions and 144 deletions

View File

@ -0,0 +1,36 @@
/*
* 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 javafx.application.Application;
import javafx.stage.Stage;
/**
* A delegate that implements JavaFX {@link Application}
*/
public interface AppDelegate {
/**
* Implement this method if you have code to run during {@link Application#init()} or
* if you need a reference to the actual {@code Application object}
* @param application a reference to the actual {@code Application} object
* @throws Exception something bad happened
*/
default void init(Application application) throws Exception {
}
void start(Stage primaryStage) throws Exception;
void stop() throws Exception;
}

View File

@ -16,136 +16,44 @@
package wallettemplate;
import com.google.common.util.concurrent.*;
import javafx.scene.input.*;
import org.bitcoinj.utils.AppDataDirectory;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Utils;
import org.bitcoinj.kits.WalletAppKit;
import org.bitcoinj.params.*;
import org.bitcoinj.script.Script;
import org.bitcoinj.utils.BriefLogFormatter;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.DeterministicSeed;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import org.bitcoinj.walletfx.utils.GuiUtils;
import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import static org.bitcoinj.walletfx.utils.GuiUtils.informationalAlert;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.script.Script;
import org.bitcoinj.walletfx.application.AppDelegate;
/**
* Proxy JavaFX {@link Application} that delegates all functionality
* to {@link WalletTemplate}
*/
public class Main extends Application {
public static NetworkParameters params = TestNet3Params.get();
public static final Script.ScriptType PREFERRED_OUTPUT_SCRIPT_TYPE = Script.ScriptType.P2WPKH;
public static final String APP_NAME = "WalletTemplate";
private static final String WALLET_FILE_NAME = APP_NAME.replaceAll("[^a-zA-Z0-9.-]", "_") + "-"
+ params.getPaymentProtocolId();
private static final NetworkParameters params = TestNet3Params.get();
private static final Script.ScriptType PREFERRED_OUTPUT_SCRIPT_TYPE = Script.ScriptType.P2WPKH;
private static final String APP_NAME = "WalletTemplate";
static WalletAppKit bitcoin;
static Main instance;
private MainController controller;
@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();
}
// 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();
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(APP_NAME).toFile();
bitcoin = new WalletAppKit(params, PREFERRED_OUTPUT_SCRIPT_TYPE, null, appDataDirectory, WALLET_FILE_NAME) {
@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(APP_NAME, "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);
}
private final AppDelegate delegate;
public static void main(String[] args) {
launch(args);
}
public Main() {
delegate = new WalletTemplate(APP_NAME, params, PREFERRED_OUTPUT_SCRIPT_TYPE);
}
@Override
public void init() throws Exception {
delegate.init(this);
}
@Override
public void start(Stage primaryStage) throws Exception {
delegate.start(primaryStage);
}
@Override
public void stop() throws Exception {
delegate.stop();
}
}

View File

@ -42,7 +42,7 @@ import org.bitcoinj.walletfx.utils.BitcoinUIModel;
import org.bitcoinj.walletfx.utils.easing.EasingMode;
import org.bitcoinj.walletfx.utils.easing.ElasticInterpolator;
import static wallettemplate.Main.bitcoin;
import static wallettemplate.WalletTemplate.bitcoin;
/**
* Gets created auto-magically by FXMLLoader via reflection. The widget fields are set to the GUI controls they're named
@ -67,7 +67,7 @@ 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(Main.APP_NAME);
addressControl.setAppName(WalletTemplate.instance.applicationName);
addressControl.setOpacity(0.0);
}

View File

@ -63,13 +63,13 @@ public class SendMoneyController implements OverlayController<SendMoneyControlle
// Called by FXMLLoader
public void initialize() {
Coin balance = Main.bitcoin.wallet().getBalance();
Coin balance = WalletTemplate.bitcoin.wallet().getBalance();
checkState(!balance.isZero());
new BitcoinAddressValidator(Main.params, address, sendBtn);
new BitcoinAddressValidator(WalletTemplate.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(Main.params, new ECKey(), Main.PREFERRED_OUTPUT_SCRIPT_TYPE).toString());
address.setPromptText(Address.fromKey(WalletTemplate.instance.params, new ECKey(), WalletTemplate.instance.preferredOutputScriptType).toString());
}
public void cancel(ActionEvent event) {
@ -80,9 +80,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(Main.params, address.getText());
Address destination = Address.fromString(WalletTemplate.instance.params, address.getText());
SendRequest req;
if (amount.equals(Main.bitcoin.wallet().getBalance()))
if (amount.equals(WalletTemplate.bitcoin.wallet().getBalance()))
req = SendRequest.emptyWallet(destination);
else
req = SendRequest.to(destination, amount);
@ -90,7 +90,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 = Main.bitcoin.wallet().sendCoins(req);
sendResult = WalletTemplate.bitcoin.wallet().sendCoins(req);
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Transaction result) {

View File

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

View File

@ -117,7 +117,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");
Main.bitcoin.wallet().encrypt(scrypt, aesKey);
WalletTemplate.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

@ -70,7 +70,7 @@ public class WalletSettingsController implements OverlayController<WalletSetting
// Note: NOT called by FXMLLoader!
public void initialize(@Nullable KeyParameter aesKey) {
DeterministicSeed seed = Main.bitcoin.wallet().getKeyChainSeed();
DeterministicSeed seed = WalletTemplate.bitcoin.wallet().getKeyChainSeed();
if (aesKey == null) {
if (seed.isEncrypted()) {
log.info("Wallet is encrypted, requesting password first.");
@ -80,7 +80,7 @@ public class WalletSettingsController implements OverlayController<WalletSetting
}
} else {
this.aesKey = aesKey;
seed = seed.decrypt(checkNotNull(Main.bitcoin.wallet().getKeyCrypter()), "", aesKey);
seed = seed.decrypt(checkNotNull(WalletTemplate.bitcoin.wallet().getKeyCrypter()), "", aesKey);
// Now we can display the wallet seed as appropriate.
passwordButton.setText("Remove password");
}
@ -158,7 +158,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 (Main.bitcoin.wallet().getBalance().value > 0) {
if (WalletTemplate.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 +180,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.
Main.bitcoin.addListener(new Service.Listener() {
WalletTemplate.bitcoin.addListener(new Service.Listener() {
@Override
public void terminated(Service.State from) {
Main.instance.setupWalletKit(seed);
Main.bitcoin.startAsync();
WalletTemplate.instance.setupWalletKit(seed);
WalletTemplate.bitcoin.startAsync();
}
}, Platform::runLater);
Main.bitcoin.stopAsync();
WalletTemplate.bitcoin.stopAsync();
}
@ -195,7 +195,7 @@ public class WalletSettingsController implements OverlayController<WalletSetting
if (aesKey == null) {
rootController.overlayUI("wallet_set_password.fxml");
} else {
Main.bitcoin.wallet().decrypt(aesKey);
WalletTemplate.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

@ -0,0 +1,157 @@
/*
* 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 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 javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import static org.bitcoinj.walletfx.utils.GuiUtils.informationalAlert;
/**
*
*/
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 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();
}
@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();
}
// 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();
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);
}
}