walletfx: WalletApplication to improve encapsulation

* Make all public fields private and wrap with accessors
* Make `WalletAppKit` (was confusingly named `bitcoin`) field a member not a static
* Rename `bitcoin` to `walletAppKit`
This commit is contained in:
Sean Gilligan 2021-09-22 14:02:42 -07:00 committed by Andreas Schildbach
parent 515a558ec2
commit 85725a18a6
6 changed files with 68 additions and 40 deletions

View file

@ -42,12 +42,12 @@ 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 static WalletApplication instance;
private WalletAppKit walletAppKit;
private final String applicationName;
private final NetworkParameters params;
private final Script.ScriptType preferredOutputScriptType;
private final String walletFileName;
private MainWindowController controller;
public WalletApplication(String applicationName, NetworkParameters params, Script.ScriptType preferredOutputScriptType) {
@ -58,6 +58,26 @@ public abstract class WalletApplication implements AppDelegate {
this.preferredOutputScriptType = preferredOutputScriptType;
}
public static WalletApplication instance() {
return instance;
}
public WalletAppKit walletAppKit() {
return walletAppKit;
}
public String applicationName() {
return applicationName;
}
public NetworkParameters params() {
return params;
}
public Script.ScriptType preferredOutputScriptType() {
return preferredOutputScriptType;
}
@Override
public void start(Stage mainWindow) throws Exception {
try {
@ -93,7 +113,7 @@ public abstract class WalletApplication implements AppDelegate {
// Create the app kit. It won't do any heavyweight initialization until after we start it.
setupWalletKit(null);
if (bitcoin.isChainFileLocked()) {
if (walletAppKit.isChainFileLocked()) {
informationalAlert("Already running", "This application is already running and cannot be started twice.");
Platform.exit();
return;
@ -103,21 +123,21 @@ public abstract class WalletApplication implements AppDelegate {
WalletSetPasswordController.estimateKeyDerivationTimeMsec();
bitcoin.addListener(new Service.Listener() {
walletAppKit.addListener(new Service.Listener() {
@Override
public void failed(Service.State from, Throwable failure) {
GuiUtils.crashAlert(failure);
}
}, Platform::runLater);
bitcoin.startAsync();
walletAppKit.startAsync();
controller.scene().getAccelerators().put(KeyCombination.valueOf("Shortcut+F"), () -> bitcoin.peerGroup().getDownloadPeer().close());
controller.scene().getAccelerators().put(KeyCombination.valueOf("Shortcut+F"), () -> walletAppKit().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) {
walletAppKit = new WalletAppKit(params, preferredOutputScriptType, null, appDataDirectory, walletFileName) {
@Override
protected void onSetupCompleted() {
Platform.runLater(controller::onBitcoinSetup);
@ -126,19 +146,19 @@ public abstract class WalletApplication implements AppDelegate {
// 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.
walletAppKit.connectToLocalHost(); // You should run a regtest mode bitcoind locally.
}
bitcoin.setDownloadListener(controller.progressBarUpdater())
walletAppKit.setDownloadListener(controller.progressBarUpdater())
.setBlockingStartup(false)
.setUserAgent(applicationName, "1.0");
if (seed != null)
bitcoin.restoreWalletFromSeed(seed);
walletAppKit.restoreWalletFromSeed(seed);
}
@Override
public void stop() throws Exception {
bitcoin.stopAsync();
bitcoin.awaitTerminated();
walletAppKit.stopAsync();
walletAppKit.awaitTerminated();
// Forcibly terminate the JVM because Orchid likes to spew non-daemon threads everywhere.
Runtime.getRuntime().exit(0);
}

View file

@ -43,8 +43,6 @@ import org.bitcoinj.walletfx.utils.BitcoinUIModel;
import org.bitcoinj.walletfx.utils.easing.EasingMode;
import org.bitcoinj.walletfx.utils.easing.ElasticInterpolator;
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
* after. This class handles all the updates and event handling for the main UI.
@ -60,15 +58,17 @@ public class MainController extends MainWindowController {
private NotificationBarPane.Item syncItem;
private static final MonetaryFormat MONETARY_FORMAT = MonetaryFormat.BTC.noCode();
private WalletApplication app;
private NotificationBarPane notificationBar;
// Called by FXMLLoader.
public void initialize() {
instance = this;
app = WalletApplication.instance();
// 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(WalletApplication.instance.applicationName);
addressControl.setAppName(app.applicationName());
addressControl.setOpacity(0.0);
}
@ -85,12 +85,12 @@ public class MainController extends MainWindowController {
// 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());
scene.getAccelerators().put(KeyCombination.valueOf("Shortcut+F"), () -> app.walletAppKit().peerGroup().getDownloadPeer().close());
}
@Override
public void onBitcoinSetup() {
model.setWallet(bitcoin.wallet());
model.setWallet(app.walletAppKit().wallet());
addressControl.addressProperty().bind(model.addressProperty());
balance.textProperty().bind(createBalanceStringBinding(model.balanceProperty()));
// Don't let the user click send money when the wallet is empty.

View file

@ -50,6 +50,7 @@ public class SendMoneyController implements OverlayController<SendMoneyControlle
public TextField amountEdit;
public Label btcLabel;
private WalletApplication app;
private OverlayableStackPaneController rootController;
private OverlayableStackPaneController.OverlayUI<? extends OverlayController<SendMoneyController>> overlayUI;
@ -64,13 +65,14 @@ public class SendMoneyController implements OverlayController<SendMoneyControlle
// Called by FXMLLoader
public void initialize() {
Coin balance = WalletApplication.bitcoin.wallet().getBalance();
app = WalletApplication.instance();
Coin balance = app.walletAppKit().wallet().getBalance();
checkState(!balance.isZero());
new BitcoinAddressValidator(WalletApplication.instance.params, address, sendBtn);
new BitcoinAddressValidator(app.params(), address, sendBtn);
new TextFieldValidator(amountEdit, text ->
!WTUtils.didThrow(() -> checkState(Coin.parseCoin(text).compareTo(balance) <= 0)));
amountEdit.setText(balance.toPlainString());
address.setPromptText(Address.fromKey(WalletApplication.instance.params, new ECKey(), WalletApplication.instance.preferredOutputScriptType).toString());
address.setPromptText(Address.fromKey(app.params(), new ECKey(), app.preferredOutputScriptType()).toString());
}
public void cancel(ActionEvent event) {
@ -81,9 +83,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(WalletApplication.instance.params, address.getText());
Address destination = Address.fromString(app.params(), address.getText());
SendRequest req;
if (amount.equals(WalletApplication.bitcoin.wallet().getBalance()))
if (amount.equals(app.walletAppKit().wallet().getBalance()))
req = SendRequest.emptyWallet(destination);
else
req = SendRequest.to(destination, amount);
@ -91,7 +93,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 = WalletApplication.bitcoin.wallet().sendCoins(req);
sendResult = app.walletAppKit().wallet().sendCoins(req);
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable Transaction result) {

View file

@ -57,6 +57,7 @@ public class WalletPasswordController implements OverlayController<WalletPasswor
@FXML GridPane widgetGrid;
@FXML Label explanationLabel;
private WalletApplication app;
private OverlayableStackPaneController rootController;
private OverlayableStackPaneController.OverlayUI<? extends OverlayController<WalletPasswordController>> overlayUI;
@ -69,6 +70,7 @@ public class WalletPasswordController implements OverlayController<WalletPasswor
}
public void initialize() {
app = WalletApplication.instance();
progressMeter.setOpacity(0);
Platform.runLater(pass1::requestFocus);
}
@ -80,13 +82,13 @@ public class WalletPasswordController implements OverlayController<WalletPasswor
return;
}
final KeyCrypterScrypt keyCrypter = (KeyCrypterScrypt) WalletApplication.bitcoin.wallet().getKeyCrypter();
final KeyCrypterScrypt keyCrypter = (KeyCrypterScrypt) app.walletAppKit().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 (WalletApplication.bitcoin.wallet().checkAESKey(aesKey)) {
if (app.walletAppKit().wallet().checkAESKey(aesKey)) {
WalletPasswordController.this.aesKey.set(aesKey);
} else {
log.warn("User entered incorrect password");
@ -123,11 +125,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()));
WalletApplication.bitcoin.wallet().setTag(TAG, bytes);
WalletApplication.instance().walletAppKit().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(WalletApplication.bitcoin.wallet().getTag(TAG).toByteArray()));
return Duration.ofMillis(Longs.fromByteArray(WalletApplication.instance().walletAppKit().wallet().getTag(TAG).toByteArray()));
}
}

View file

@ -46,6 +46,7 @@ public class WalletSetPasswordController implements OverlayController<WalletSetP
public Button closeButton;
public Label explanationLabel;
private WalletApplication app;
private OverlayableStackPaneController rootController;
private OverlayableStackPaneController.OverlayUI<? extends OverlayController<WalletSetPasswordController>> overlayUI;
// These params were determined empirically on a top-range (as of 2014) MacBook Pro with native scrypt support,
@ -64,6 +65,7 @@ public class WalletSetPasswordController implements OverlayController<WalletSetP
}
public void initialize() {
app = WalletApplication.instance();
progressMeter.setOpacity(0);
}
@ -118,7 +120,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");
WalletApplication.bitcoin.wallet().encrypt(scrypt, aesKey);
app.walletAppKit().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

@ -58,6 +58,7 @@ public class WalletSettingsController implements OverlayController<WalletSetting
@FXML TextArea wordsArea;
@FXML Button restoreButton;
private WalletApplication app;
private OverlayableStackPaneController rootController;
private OverlayableStackPaneController.OverlayUI<? extends OverlayController<WalletSettingsController>> overlayUI;
@ -71,7 +72,8 @@ public class WalletSettingsController implements OverlayController<WalletSetting
// Note: NOT called by FXMLLoader!
public void initialize(@Nullable KeyParameter aesKey) {
DeterministicSeed seed = WalletApplication.bitcoin.wallet().getKeyChainSeed();
app = WalletApplication.instance();
DeterministicSeed seed = app.walletAppKit().wallet().getKeyChainSeed();
if (aesKey == null) {
if (seed.isEncrypted()) {
log.info("Wallet is encrypted, requesting password first.");
@ -81,7 +83,7 @@ public class WalletSettingsController implements OverlayController<WalletSetting
}
} else {
this.aesKey = aesKey;
seed = seed.decrypt(checkNotNull(WalletApplication.bitcoin.wallet().getKeyCrypter()), "", aesKey);
seed = seed.decrypt(checkNotNull(app.walletAppKit().wallet().getKeyCrypter()), "", aesKey);
// Now we can display the wallet seed as appropriate.
passwordButton.setText("Remove password");
}
@ -159,7 +161,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 (WalletApplication.bitcoin.wallet().getBalance().value > 0) {
if (app.walletAppKit().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.");
@ -181,14 +183,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.
WalletApplication.bitcoin.addListener(new Service.Listener() {
app.walletAppKit().addListener(new Service.Listener() {
@Override
public void terminated(Service.State from) {
WalletApplication.instance.setupWalletKit(seed);
WalletApplication.bitcoin.startAsync();
app.setupWalletKit(seed);
app.walletAppKit().startAsync();
}
}, Platform::runLater);
WalletApplication.bitcoin.stopAsync();
app.walletAppKit().stopAsync();
}
@ -196,7 +198,7 @@ public class WalletSettingsController implements OverlayController<WalletSetting
if (aesKey == null) {
rootController.overlayUI("wallet_set_password.fxml");
} else {
WalletApplication.bitcoin.wallet().decrypt(aesKey);
app.walletAppKit().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;