/**
* Author: Mike Hearn <mhearn@bitcoinfoundation.org>
*
* 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 net.plan99.payfile.gui;
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.params.TestNet3Params;
import com.google.bitcoin.protocols.channels.StoredPaymentChannelClientStates;
import com.google.bitcoin.store.BlockStoreException;
import com.google.bitcoin.utils.BriefLogFormatter;
import com.google.bitcoin.utils.Threading;
import com.google.common.base.Throwables;
import com.google.common.net.HostAndPort;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import joptsimple.BuiltinHelpFormatter;
import joptsimple.OptionException;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import net.plan99.payfile.client.PayFileClient;
import net.plan99.payfile.gui.utils.TextFieldValidator;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
import static joptsimple.util.RegexMatcher.regex;
import static net.plan99.payfile.gui.utils.GuiUtils.*;
import static net.plan99.payfile.utils.Exceptions.evalUnchecked;
import static net.plan99.payfile.utils.Exceptions.runUnchecked;
// To do list:
//
// Payments:
// - Progress indicator for negotiating a payment channel?
// - Bug: If the server fails to broadcast the contract tx then the client gets out of sync with the server.
//
// Misc code quality:
// - Consider switching to P2Proto (question: how to do SSL with that?). Simplifies the core protocol.
// - SSL support
//
// Generic UI:
// - Solve the Mac menubar issue. Port the Mac specific tweaks to wallet-template.
// - Write a test plan that exercises every reasonable path through the app and test it.
// - Get an Apple developer ID and a Windows codesigning cert.
// - Find/beg/buy/borrow/steal a nice icon.
// - Find a way to dual boot Windows on my laptop.
// - Build, sign and test native packages!
//
// Future ideas:
// - Merkle tree validators for files, to avoid a server maliciously serving junk instead of the real deal.
public class Main extends Application {
public static final String APP_NAME = "PayFile";
public static final int CONNECT_TIMEOUT_MSEC = 2000;
public static NetworkParameters params;
public static WalletAppKit bitcoin;
public static Main instance;
public static PayFileClient client;
public static HostAndPort serverAddress;
private static String filePrefix;
private StackPane uiStack;
public Pane mainUI;
public Controller controller;
public Stage mainWindow;
@Override
public void start(Stage mainWindow) throws Exception {
instance = this;
// Show the crash dialog for any exceptions that we don't handle and that hit the main loop.
handleCrashesOnThisThread();
try {
init(mainWindow);
} catch (Throwable t) {
// Nicer message for the case where the block store file is locked.
if (Throwables.getRootCause(t) instanceof BlockStoreException) {
informationalAlert("Already running", "This application is already running and cannot be started twice.");
} else {
throw t;
}
}
}
private void init(Stage mainWindow) throws IOException {
this.mainWindow = mainWindow;
// commented out for now as Modena looks better, but might want to bring this back.
/* if (System.getProperty("os.name").toLowerCase().contains("mac")) {
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 = 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.
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. Also note that this doesn't affect the default executor for ListenableFutures.
// That must be specified each time.
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("."), filePrefix + APP_NAME ) {
@Override
protected void addWalletExtensions() throws Exception {
super.addWalletExtensions();
wallet().addExtension(new StoredPaymentChannelClientStates(wallet(), peerGroup()));
}
};
if (params == RegTestParams.get()) {
bitcoin.connectToLocalHost(); // You should run a regtest mode bitcoind locally.
} else if (params == MainNetParams.get()) {
// Checkpoints are block headers that ship inside our app: for a new user, we pick the last header
// in the checkpoints file and then download the rest from the network. It makes things much faster.
// Checkpoint files are made using the BuildCheckpoints tool and usually we have to download the
// last months worth or more (takes a few seconds).
bitcoin.setCheckpoints(getClass().getResourceAsStream("checkpoints"));
}
// Now configure and start the appkit. It won't block for very long.
bitcoin.setDownloadListener(controller.progressBarUpdater())
.setBlockingStartup(false)
.setUserAgent("PayFile Client", "1.0")
.startAndWait();
// Don't make the user wait for confirmations for now, as the intention is they're sending it their own money!
bitcoin.wallet().allowSpendingUnconfirmedTransactions();
System.out.println(bitcoin.wallet());
controller.onBitcoinSetup();
overlayUI("connect_server.fxml");
mainUI.setVisible(false);
mainWindow.show();
}
public class OverlayUI<T> {
public Node ui;
public T controller;
public OverlayUI(Node ui, T controller) {
this.ui = ui;
this.controller = controller;
}
public void show() {
blurOut(mainUI);
uiStack.getChildren().add(ui);
fadeIn(ui);
}
public void done() {
checkGuiThread();
fadeOutAndRemove(ui, uiStack);
blurIn(mainUI);
this.ui = null;
this.controller = null;
}
}
public <T> OverlayUI<T> overlayUI(Node node, T controller) {
checkGuiThread();
OverlayUI<T> pair = new OverlayUI<>(node, controller);
// Auto-magically set the overlayUi member, if it's there.
runUnchecked(() -> controller.getClass().getDeclaredField("overlayUi").set(controller, pair));
pair.show();
return pair;
}
/** Loads the FXML file with the given name, blurs out the main UI and puts this one on top. */
public <T> OverlayUI<T> overlayUI(String name) {
return evalUnchecked(() -> {
checkGuiThread();
// Load the UI from disk.
URL location = getClass().getResource(name);
FXMLLoader loader = new FXMLLoader(location);
Pane ui = loader.load();
T controller = loader.getController();
OverlayUI<T> pair = new OverlayUI<>(ui, controller);
// Auto-magically set the overlayUi member, if it's there.
controller.getClass().getDeclaredField("overlayUi").set(controller, pair);
pair.show();
return pair;
});
}
public static CompletableFuture<PayFileClient> connect(HostAndPort server) {
serverAddress = server;
return connect(serverAddress, CONNECT_TIMEOUT_MSEC);
}
public static CompletableFuture<PayFileClient> connect(HostAndPort server, int timeoutMsec) {
return CompletableFuture.supplyAsync(() ->
evalUnchecked(() -> {
final InetSocketAddress address = new InetSocketAddress(server.getHostText(), server.getPort());
final Socket socket = new Socket();
socket.connect(address, timeoutMsec);
return new PayFileClient(socket, bitcoin.wallet());
})
);
}
@Override
public void stop() throws Exception {
bitcoin.stopAndWait();
super.stop();
}
public static void main(String[] args) throws Exception {
// allow client to choose another network for testing by passing through an argument.
OptionParser parser = new OptionParser();
parser.accepts("network").withRequiredArg().withValuesConvertedBy(regex("(mainnet)|(testnet)|(regtest)")).defaultsTo("mainnet");
parser.accepts("help").forHelp();
parser.formatHelpWith(new BuiltinHelpFormatter(120, 10));
OptionSet options;
try {
options = parser.parse(args);
} catch (OptionException e) {
System.err.println(e.getMessage());
System.err.println("");
parser.printHelpOn(System.err);
return;
}
if (options.has("help")) {
parser.printHelpOn(System.out);
return;
}
if (options.valueOf("network").equals(("testnet"))) {
params = TestNet3Params.get();
filePrefix = "testnet-";
} else if (options.valueOf("network").equals(("mainnet"))) {
params = MainNetParams.get();
filePrefix = "";
} else if (options.valueOf("network").equals(("regtest"))) {
params = RegTestParams.get();
filePrefix = "regtest-";
}
launch(args);
}
}