add built in tor

This commit is contained in:
Craig Raw 2020-09-14 10:51:38 +02:00
parent 4bc446724d
commit 2e0ca1b4fa
17 changed files with 190 additions and 19 deletions

View file

@ -9,7 +9,7 @@ def sparrowVersion = '0.9.3'
def os = org.gradle.internal.os.OperatingSystem.current() def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName() def osName = os.getFamilyName()
if(os.macOsX) { if(os.macOsX) {
osName = "mac" osName = "osx"
} }
group "com.sparrowwallet" group "com.sparrowwallet"
@ -19,6 +19,7 @@ repositories {
mavenCentral() mavenCentral()
maven { url 'https://oss.sonatype.org/content/groups/public' } maven { url 'https://oss.sonatype.org/content/groups/public' }
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' } maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
maven { url 'https://jitpack.io' }
} }
tasks.withType(AbstractArchiveTask) { tasks.withType(AbstractArchiveTask) {
@ -54,6 +55,7 @@ dependencies {
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') { implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
exclude group: 'com.nativelibs4java', module: 'bridj' exclude group: 'com.nativelibs4java', module: 'bridj'
} }
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8")
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7') implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
implementation('org.controlsfx:controlsfx:11.0.2' ) { implementation('org.controlsfx:controlsfx:11.0.2' ) {
exclude group: 'org.openjfx', module: 'javafx-base' exclude group: 'org.openjfx', module: 'javafx-base'
@ -80,8 +82,8 @@ compileJava {
processResources { processResources {
doLast { doLast {
delete fileTree("$buildDir/resources/main/external").matching { delete fileTree("$buildDir/resources/main/native").matching {
exclude "$osName/**" exclude "${osName}/**"
} }
} }
} }
@ -101,7 +103,8 @@ run {
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx", "--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx", "--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow"] "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow"]
if(os.macOsX) { if(os.macOsX) {
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png", applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow.png",
@ -118,6 +121,7 @@ jlink {
requires 'javafx.base' requires 'javafx.base'
requires 'com.fasterxml.jackson.databind' requires 'com.fasterxml.jackson.databind'
requires 'jdk.crypto.cryptoki' requires 'jdk.crypto.cryptoki'
requires 'java.management'
} }
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png'] options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']
@ -139,6 +143,7 @@ jlink {
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module",
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow", "--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
"--add-reads=com.sparrowwallet.merged.module=java.desktop"] "--add-reads=com.sparrowwallet.merged.module=java.desktop"]
} }
addExtraDependencies("javafx") addExtraDependencies("javafx")
@ -160,7 +165,7 @@ jlink {
} }
if(os.macOsX) { if(os.macOsX) {
installerOptions += ['--mac-sign', '--mac-signing-key-user-name', 'Craig Raw (UPLVMSK9D7)'] installerOptions += ['--mac-sign', '--mac-signing-key-user-name', 'Craig Raw (UPLVMSK9D7)']
imageOptions += ['--icon', 'src/main/deploy/package/mac/sparrow.icns', '--resource-dir', 'src/main/deploy/package/mac/'] imageOptions += ['--icon', 'src/main/deploy/package/osx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/osx/']
installerType = "dmg" installerType = "dmg"
} }
} }

View file

@ -280,6 +280,13 @@ public class AppController implements Initializable {
connectionService.setPeriod(new Duration(SERVER_PING_PERIOD)); connectionService.setPeriod(new Duration(SERVER_PING_PERIOD));
connectionService.setRestartOnFailure(true); connectionService.setRestartOnFailure(true);
EventManager.get().register(connectionService);
connectionService.statusProperty().addListener((observable, oldValue, newValue) -> {
if(connectionService.isRunning()) {
EventManager.get().post(new StatusEvent(newValue));
}
});
connectionService.setOnSucceeded(successEvent -> { connectionService.setOnSucceeded(successEvent -> {
changeMode = false; changeMode = false;
onlineProperty.setValue(true); onlineProperty.setValue(true);
@ -350,8 +357,8 @@ public class AppController implements Initializable {
tk.createQuitMenuItem(MainApp.APP_NAME)); tk.createQuitMenuItem(MainApp.APP_NAME));
tk.setApplicationMenu(defaultApplicationMenu); tk.setApplicationMenu(defaultApplicationMenu);
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("macHide")); fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("macHide")); helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
} }
} }
@ -1155,7 +1162,7 @@ public class AppController implements Initializable {
public void statusUpdated(StatusEvent event) { public void statusUpdated(StatusEvent event) {
statusBar.setText(event.getStatus()); statusBar.setText(event.getStatus());
PauseTransition wait = new PauseTransition(Duration.seconds(10)); PauseTransition wait = new PauseTransition(Duration.seconds(20));
wait.setOnFinished((e) -> { wait.setOnFinished((e) -> {
if(statusBar.getText().equals(event.getStatus())) { if(statusBar.getText().equals(event.getStatus())) {
statusBar.setText(""); statusBar.setText("");

View file

@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
public class TorStatusEvent {
private final String status;
public TorStatusEvent(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
}

View file

@ -23,7 +23,6 @@ import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions; import java.nio.file.attribute.PosixFilePermissions;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream; import java.util.zip.ZipInputStream;
@ -200,7 +199,7 @@ public class Hwi {
File hwiExecutable = Config.get().getHwi(); File hwiExecutable = Config.get().getHwi();
if(hwiExecutable != null && hwiExecutable.exists()) { if(hwiExecutable != null && hwiExecutable.exists()) {
if(command.isTestFirst() && (!hwiExecutable.getAbsolutePath().contains(TEMP_FILE_PREFIX) || !testHwi(hwiExecutable))) { if(command.isTestFirst() && (!hwiExecutable.getAbsolutePath().contains(TEMP_FILE_PREFIX) || !testHwi(hwiExecutable))) {
if(Platform.getCurrent().getPlatformId().toLowerCase().equals("mac")) { if(Platform.getCurrent() == Platform.OSX) {
deleteDirectory(hwiExecutable.getParentFile()); deleteDirectory(hwiExecutable.getParentFile());
} else { } else {
hwiExecutable.delete(); hwiExecutable.delete();
@ -218,7 +217,7 @@ public class Hwi {
//The check will still happen on first invocation, but will not thereafter //The check will still happen on first invocation, but will not thereafter
//See https://github.com/bitcoin-core/HWI/issues/327 for details //See https://github.com/bitcoin-core/HWI/issues/327 for details
if(platform == Platform.OSX) { if(platform == Platform.OSX) {
InputStream inputStream = Hwi.class.getResourceAsStream("/external/mac/hwi-1.1.2-mac-amd64-signed.zip"); InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/hwi-1.1.2-mac-amd64-signed.zip");
Path tempHwiDirPath = Files.createTempDirectory(TEMP_FILE_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable)); Path tempHwiDirPath = Files.createTempDirectory(TEMP_FILE_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
File tempHwiDir = tempHwiDirPath.toFile(); File tempHwiDir = tempHwiDirPath.toFile();
//tempHwiDir.deleteOnExit(); //tempHwiDir.deleteOnExit();
@ -249,10 +248,10 @@ public class Hwi {
InputStream inputStream; InputStream inputStream;
Path tempExecPath; Path tempExecPath;
if(platform == Platform.WINDOWS) { if(platform == Platform.WINDOWS) {
inputStream = Hwi.class.getResourceAsStream("/external/windows/hwi.exe"); inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null); tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null);
} else { } else {
inputStream = Hwi.class.getResourceAsStream("/external/linux/hwi"); inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable)); tempExecPath = Files.createTempFile(TEMP_FILE_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
} }

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.net; package com.sparrowwallet.sparrow.net;
import com.github.arteam.simplejsonrpc.client.Transport; import com.github.arteam.simplejsonrpc.client.Transport;
import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort; import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.KeyPurpose; import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
@ -8,8 +9,11 @@ import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.event.ConnectionEvent; import com.sparrowwallet.sparrow.event.ConnectionEvent;
import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent; import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import com.sparrowwallet.sparrow.io.Config; import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.SendController; import com.sparrowwallet.sparrow.wallet.SendController;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.ScheduledService; import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service; import javafx.concurrent.Service;
import javafx.concurrent.Task; import javafx.concurrent.Task;
@ -578,6 +582,7 @@ public class ElectrumServer {
private boolean firstCall = true; private boolean firstCall = true;
private Thread reader; private Thread reader;
private long feeRatesRetrievedAt; private long feeRatesRetrievedAt;
private StringProperty statusProperty = new SimpleStringProperty();
public ConnectionService() { public ConnectionService() {
this(true); this(true);
@ -675,6 +680,15 @@ public class ElectrumServer {
public void uncaughtException(Thread t, Throwable e) { public void uncaughtException(Thread t, Throwable e) {
log.error("Uncaught error in ConnectionService", e); log.error("Uncaught error in ConnectionService", e);
} }
@Subscribe
public void torStatus(TorStatusEvent event) {
statusProperty.set(event.getStatus());
}
public StringProperty statusProperty() {
return statusProperty;
}
} }
public static class ReadRunnable implements Runnable { public static class ReadRunnable implements Runnable {

View file

@ -14,12 +14,16 @@ public enum Protocol {
TCP { TCP {
@Override @Override
public Transport getTransport(HostAndPort server) { public Transport getTransport(HostAndPort server) {
if(isOnionAddress(server)) {
return new TorTcpTransport(server);
}
return new TcpTransport(server); return new TcpTransport(server);
} }
@Override @Override
public Transport getTransport(HostAndPort server, File serverCert) { public Transport getTransport(HostAndPort server, File serverCert) {
return new TcpTransport(server); return getTransport(server);
} }
@Override @Override
@ -35,11 +39,19 @@ public enum Protocol {
SSL { SSL {
@Override @Override
public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException { public Transport getTransport(HostAndPort server) throws KeyManagementException, NoSuchAlgorithmException {
if(isOnionAddress(server)) {
return new TorTcpOverTlsTransport(server);
}
return new TcpOverTlsTransport(server); return new TcpOverTlsTransport(server);
} }
@Override @Override
public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException { public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
if(isOnionAddress(server)) {
return new TorTcpOverTlsTransport(server, serverCert);
}
return new TcpOverTlsTransport(server, serverCert); return new TcpOverTlsTransport(server, serverCert);
} }
@ -82,6 +94,10 @@ public enum Protocol {
return toUrlString() + hostAndPort.toString(); return toUrlString() + hostAndPort.toString();
} }
public boolean isOnionAddress(HostAndPort server) {
return server.getHost().toLowerCase().endsWith(".onion");
}
public static Protocol getProtocol(String url) { public static Protocol getProtocol(String url) {
if(url.startsWith("tcp://")) { if(url.startsWith("tcp://")) {
return TCP; return TCP;

View file

@ -0,0 +1,49 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLSocket;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
public class TorTcpOverTlsTransport extends TcpOverTlsTransport {
private static final Logger log = LoggerFactory.getLogger(TorTcpOverTlsTransport.class);
public TorTcpOverTlsTransport(HostAndPort server) throws NoSuchAlgorithmException, KeyManagementException {
super(server);
}
public TorTcpOverTlsTransport(HostAndPort server, File crtFile) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
super(server, crtFile);
}
@Override
protected Socket createSocket() throws IOException {
TorTcpTransport torTcpTransport = new TorTcpTransport(server);
Socket socket = torTcpTransport.createSocket();
try {
Field socketField = socket.getClass().getDeclaredField("socket");
socketField.setAccessible(true);
Socket innerSocket = (Socket)socketField.get(socket);
Field connectedField = innerSocket.getClass().getSuperclass().getDeclaredField("connected");
connectedField.setAccessible(true);
connectedField.set(innerSocket, true);
} catch(Exception e) {
log.error("Could not set socket connected status", e);
}
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, server.getHost(), server.getPortOrDefault(DEFAULT_PORT), true);
sslSocket.startHandshake();
return sslSocket;
}
}

View file

@ -0,0 +1,55 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.StatusEvent;
import com.sparrowwallet.sparrow.event.TorStatusEvent;
import javafx.application.Platform;
import org.berndpruenster.netlayer.tor.*;
import java.io.*;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.LinkedHashMap;
public class TorTcpTransport extends TcpTransport {
public static final String TOR_DIR_PREFIX = "tor";
public TorTcpTransport(HostAndPort server) {
super(server);
}
@Override
protected Socket createSocket() throws IOException {
if(Tor.getDefault() == null) {
Platform.runLater(() -> {
String status = "Starting Tor...";
EventManager.get().post(new TorStatusEvent(status));
});
Path path = Files.createTempDirectory(TOR_DIR_PREFIX);
File torInstallDir = path.toFile();
torInstallDir.deleteOnExit();
try {
LinkedHashMap<String, String> torrcOptionsMap = new LinkedHashMap<>();
torrcOptionsMap.put("DisableNetwork", "0");
Torrc override = new Torrc(torrcOptionsMap);
NativeTor nativeTor = new NativeTor(torInstallDir, Collections.emptyList(), override);
Tor.setDefault(nativeTor);
} catch(TorCtlException e) {
e.printStackTrace();
throw new IOException(e);
}
}
Platform.runLater(() -> {
String status = "Tor running, connecting to " + server.toString() + "...";
EventManager.get().post(new TorStatusEvent(status));
});
return new TorSocket(server.getHost(), server.getPort(), "sparrow");
}
}

View file

@ -28,6 +28,8 @@ import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator; import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration; import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLHandshakeException;
import java.io.File; import java.io.File;
@ -36,6 +38,8 @@ import java.security.cert.CertificateFactory;
import java.util.List; import java.util.List;
public class ServerPreferencesController extends PreferencesDetailController { public class ServerPreferencesController extends PreferencesDetailController {
private static final Logger log = LoggerFactory.getLogger(ServerPreferencesController.class);
@FXML @FXML
private TextField host; private TextField host;
@ -140,13 +144,20 @@ public class ServerPreferencesController extends PreferencesDetailController {
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null)); testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null));
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false); ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService(false);
connectionService.setPeriod(Duration.minutes(1)); connectionService.setPeriod(Duration.ZERO);
EventManager.get().register(connectionService);
connectionService.statusProperty().addListener((observable, oldValue, newValue) -> {
testResults.setText(testResults.getText() + "\n" + newValue);
});
connectionService.setOnSucceeded(successEvent -> { connectionService.setOnSucceeded(successEvent -> {
EventManager.get().unregister(connectionService);
ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue(); ConnectionEvent connectionEvent = (ConnectionEvent)connectionService.getValue();
showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner()); showConnectionSuccess(connectionEvent.getServerVersion(), connectionEvent.getServerBanner());
connectionService.cancel(); connectionService.cancel();
}); });
connectionService.setOnFailed(workerStateEvent -> { connectionService.setOnFailed(workerStateEvent -> {
EventManager.get().unregister(connectionService);
showConnectionFailure(workerStateEvent); showConnectionFailure(workerStateEvent);
connectionService.cancel(); connectionService.cancel();
}); });
@ -230,6 +241,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
private void showConnectionFailure(WorkerStateEvent failEvent) { private void showConnectionFailure(WorkerStateEvent failEvent) {
Throwable e = failEvent.getSource().getException(); Throwable e = failEvent.getSource().getException();
log.error("Connection error", e);
String reason = e.getCause() != null ? e.getCause().getMessage() : e.getMessage(); String reason = e.getCause() != null ? e.getCause().getMessage() : e.getMessage();
if(e.getCause() != null && e.getCause() instanceof SSLHandshakeException) { if(e.getCause() != null && e.getCause() instanceof SSLHandshakeException) {
reason = "SSL Handshake Error\n" + reason; reason = "SSL Handshake Error\n" + reason;

View file

@ -20,6 +20,7 @@ open module com.sparrowwallet.sparrow {
requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.databind;
requires cbor; requires cbor;
requires webcam.capture; requires webcam.capture;
requires netlayer.jpms;
requires centerdevice.nsmenufx; requires centerdevice.nsmenufx;
requires slf4j.api; requires slf4j.api;
} }

View file

@ -29,11 +29,11 @@
<SeparatorMenuItem /> <SeparatorMenuItem />
<MenuItem mnemonicParsing="false" text="Import Wallet..." onAction="#importWallet"/> <MenuItem mnemonicParsing="false" text="Import Wallet..." onAction="#importWallet"/>
<MenuItem fx:id="exportWallet" mnemonicParsing="false" text="Export Wallet..." onAction="#exportWallet"/> <MenuItem fx:id="exportWallet" mnemonicParsing="false" text="Export Wallet..." onAction="#exportWallet"/>
<SeparatorMenuItem styleClass="macHide" /> <SeparatorMenuItem styleClass="osxHide" />
<MenuItem styleClass="macHide" mnemonicParsing="false" text="Preferences..." onAction="#openPreferences"/> <MenuItem styleClass="osxHide" mnemonicParsing="false" text="Preferences..." onAction="#openPreferences"/>
<SeparatorMenuItem /> <SeparatorMenuItem />
<MenuItem mnemonicParsing="false" text="Close Tab" onAction="#closeTab"/> <MenuItem mnemonicParsing="false" text="Close Tab" onAction="#closeTab"/>
<MenuItem styleClass="macHide" mnemonicParsing="false" text="Quit" onAction="#quit"/> <MenuItem styleClass="osxHide" mnemonicParsing="false" text="Quit" onAction="#quit"/>
</items> </items>
</Menu> </Menu>
<fx:define> <fx:define>
@ -67,7 +67,7 @@
<MenuItem mnemonicParsing="false" text="Sign/Verify Message" onAction="#signVerifyMessage"/> <MenuItem mnemonicParsing="false" text="Sign/Verify Message" onAction="#signVerifyMessage"/>
</Menu> </Menu>
<Menu fx:id="helpMenu" mnemonicParsing="false" text="Help"> <Menu fx:id="helpMenu" mnemonicParsing="false" text="Help">
<MenuItem styleClass="macHide" mnemonicParsing="false" text="About Sparrow" onAction="#showAbout"/> <MenuItem styleClass="osxHide" mnemonicParsing="false" text="About Sparrow" onAction="#showAbout"/>
</Menu> </Menu>
</menus> </menus>
</MenuBar> </MenuBar>