Merge branch 'master' into whirlpool-1.0.0

This commit is contained in:
Craig Raw 2024-03-29 10:12:11 +02:00
commit 8e66db0237
33 changed files with 1102 additions and 707 deletions

View file

@ -3,11 +3,11 @@ import java.awt.GraphicsEnvironment
plugins {
id 'application'
id 'extra-java-module-info'
id 'org.openjfx.javafxplugin' version '0.1.0'
id 'org-openjfx-javafxplugin'
id 'org.beryx.jlink' version '3.0.1'
}
def sparrowVersion = '1.8.3'
def sparrowVersion = '1.8.5'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@ -161,7 +161,7 @@ processResources {
test {
useJUnitPlatform()
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson"]
}
application {
@ -247,6 +247,8 @@ jlink {
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor",
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg",
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider",
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
if(os.windows) {
@ -650,18 +652,6 @@ extraJavaModuleInfo {
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
exports('com.beust.jcommander')
}
module('pgpainless-core-1.6.6.jar', 'org.pgpainless.core', '1.6.6') {
exports('org.pgpainless')
exports('org.pgpainless.key')
exports('org.pgpainless.key.parsing')
exports('org.pgpainless.decryption_verification')
exports('org.pgpainless.exception')
exports('org.pgpainless.signature')
exports('org.pgpainless.util')
requires('org.bouncycastle.provider')
requires('org.bouncycastle.pg')
requires('org.slf4j')
}
module('jzlib-1.1.3.jar', 'com.jcraft.jzlib', '1.1.3') {
exports('com.jcraft.jzlib')
}

View file

@ -3,6 +3,8 @@ plugins {
}
dependencies {
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
implementation 'org.javamodularity:moduleplugin:1.8.14'
implementation 'org.ow2.asm:asm:9.6'
}
@ -20,5 +22,9 @@ gradlePlugin {
id = "extra-java-module-info"
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
}
register("org-openjfx-javafxplugin") {
id = "org-openjfx-javafxplugin"
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
}
}
}

View file

@ -143,7 +143,7 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
JarEntry jarEntry = inputStream.getNextJarEntry();
while (jarEntry != null) {
if(!jarEntry.getName().equals("module-info.class") && !jarEntry.getName().equals("org/bouncycastle/CachingBcPublicKeyDataDecryptorFactory.class")) {
if(!jarEntry.getName().equals("module-info.class")) {
outputStream.putNextEntry(jarEntry);
outputStream.write(inputStream.readAllBytes());
outputStream.closeEntry();

View file

@ -0,0 +1,114 @@
/*
* Copyright (c) 2018, 2020, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openjfx.gradle;
import org.gradle.api.GradleException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public enum JavaFXModule {
BASE,
GRAPHICS(BASE),
CONTROLS(BASE, GRAPHICS),
FXML(BASE, GRAPHICS),
MEDIA(BASE, GRAPHICS),
SWING(BASE, GRAPHICS),
WEB(BASE, CONTROLS, GRAPHICS, MEDIA);
static final String PREFIX_MODULE = "javafx.";
private static final String PREFIX_ARTIFACT = "javafx-";
private List<JavaFXModule> dependentModules;
JavaFXModule(JavaFXModule...dependentModules) {
this.dependentModules = List.of(dependentModules);
}
public static Optional<JavaFXModule> fromModuleName(String moduleName) {
return Stream.of(JavaFXModule.values())
.filter(javaFXModule -> moduleName.equals(javaFXModule.getModuleName()))
.findFirst();
}
public String getModuleName() {
return PREFIX_MODULE + name().toLowerCase(Locale.ROOT);
}
public String getModuleJarFileName() {
return getModuleName() + ".jar";
}
public String getArtifactName() {
return PREFIX_ARTIFACT + name().toLowerCase(Locale.ROOT);
}
public boolean compareJarFileName(JavaFXPlatform platform, String jarFileName) {
Pattern p = Pattern.compile(getArtifactName() + "-.+-" + platform.getClassifier() + "\\.jar");
return p.matcher(jarFileName).matches();
}
public static Set<JavaFXModule> getJavaFXModules(List<String> moduleNames) {
validateModules(moduleNames);
return moduleNames.stream()
.map(JavaFXModule::fromModuleName)
.flatMap(Optional::stream)
.flatMap(javaFXModule -> javaFXModule.getMavenDependencies().stream())
.collect(Collectors.toSet());
}
public static void validateModules(List<String> moduleNames) {
var invalidModules = moduleNames.stream()
.filter(module -> JavaFXModule.fromModuleName(module).isEmpty())
.collect(Collectors.toList());
if (! invalidModules.isEmpty()) {
throw new GradleException("Found one or more invalid JavaFX module names: " + invalidModules);
}
}
public List<JavaFXModule> getDependentModules() {
return dependentModules;
}
public List<JavaFXModule> getMavenDependencies() {
List<JavaFXModule> dependencies = new ArrayList<>(dependentModules);
dependencies.add(0, this);
return dependencies;
}
}

View file

@ -0,0 +1,164 @@
/*
* Copyright (c) 2018, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openjfx.gradle;
import org.gradle.api.Project;
import org.gradle.api.artifacts.repositories.FlatDirectoryArtifactRepository;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.openjfx.gradle.JavaFXModule.PREFIX_MODULE;
public class JavaFXOptions {
private static final String MAVEN_JAVAFX_ARTIFACT_GROUP_ID = "org.openjfx";
private static final String JAVAFX_SDK_LIB_FOLDER = "lib";
private final Project project;
private final JavaFXPlatform platform;
private String version = "16";
private String sdk;
private String configuration = "implementation";
private String lastUpdatedConfiguration;
private List<String> modules = new ArrayList<>();
private FlatDirectoryArtifactRepository customSDKArtifactRepository;
public JavaFXOptions(Project project) {
this.project = project;
this.platform = JavaFXPlatform.detect(project);
}
public JavaFXPlatform getPlatform() {
return platform;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
updateJavaFXDependencies();
}
/**
* If set, the JavaFX modules will be taken from this local
* repository, and not from Maven Central
* @param sdk, the path to the local JavaFX SDK folder
*/
public void setSdk(String sdk) {
this.sdk = sdk;
updateJavaFXDependencies();
}
public String getSdk() {
return sdk;
}
/** Set the configuration name for dependencies, e.g.
* 'implementation', 'compileOnly' etc.
* @param configuration The configuration name for dependencies
*/
public void setConfiguration(String configuration) {
this.configuration = configuration;
updateJavaFXDependencies();
}
public String getConfiguration() {
return configuration;
}
public List<String> getModules() {
return modules;
}
public void setModules(List<String> modules) {
this.modules = modules;
updateJavaFXDependencies();
}
public void modules(String...moduleNames) {
setModules(List.of(moduleNames));
}
private void updateJavaFXDependencies() {
clearJavaFXDependencies();
String configuration = getConfiguration();
JavaFXModule.getJavaFXModules(this.modules).stream()
.sorted()
.forEach(javaFXModule -> {
if (customSDKArtifactRepository != null) {
project.getDependencies().add(configuration, Map.of("name", javaFXModule.getModuleName()));
} else {
project.getDependencies().add(configuration,
String.format("%s:%s:%s:%s", MAVEN_JAVAFX_ARTIFACT_GROUP_ID, javaFXModule.getArtifactName(),
getVersion(), getPlatform().getClassifier()));
}
});
lastUpdatedConfiguration = configuration;
}
private void clearJavaFXDependencies() {
if (customSDKArtifactRepository != null) {
project.getRepositories().remove(customSDKArtifactRepository);
customSDKArtifactRepository = null;
}
if (sdk != null && ! sdk.isEmpty()) {
Map<String, String> dirs = new HashMap<>();
dirs.put("name", "customSDKArtifactRepository");
if (sdk.endsWith(File.separator)) {
dirs.put("dirs", sdk + JAVAFX_SDK_LIB_FOLDER);
} else {
dirs.put("dirs", sdk + File.separator + JAVAFX_SDK_LIB_FOLDER);
}
customSDKArtifactRepository = project.getRepositories().flatDir(dirs);
}
if (lastUpdatedConfiguration == null) {
return;
}
var configuration = project.getConfigurations().findByName(lastUpdatedConfiguration);
if (configuration != null) {
if (customSDKArtifactRepository != null) {
configuration.getDependencies()
.removeIf(dependency -> dependency.getName().startsWith(PREFIX_MODULE));
}
configuration.getDependencies()
.removeIf(dependency -> MAVEN_JAVAFX_ARTIFACT_GROUP_ID.equals(dependency.getGroup()));
}
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright (c) 2018, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openjfx.gradle;
import com.google.gradle.osdetector.OsDetector;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import java.awt.*;
import java.util.Arrays;
import java.util.stream.Collectors;
public enum JavaFXPlatform {
LINUX("linux", "linux-x86_64"),
LINUX_MONOCLE("linux-monocle", "linux-x86_64-monocle"),
LINUX_AARCH64("linux-aarch64", "linux-aarch_64"),
LINUX_AARCH64_MONOCLE("linux-aarch64-monocle", "linux-aarch_64-monocle"),
WINDOWS("win", "windows-x86_64"),
WINDOWS_MONOCLE("win-monocle", "windows-x86_64-monocle"),
OSX("mac", "osx-x86_64"),
OSX_MONOCLE("mac-monocle", "osx-x86_64-monocle"),
OSX_AARCH64("mac-aarch64", "osx-aarch_64"),
OSX_AARCH64_MONOCLE("mac-aarch64-monocle", "osx-aarch_64-monocle");
private final String classifier;
private final String osDetectorClassifier;
JavaFXPlatform( String classifier, String osDetectorClassifier ) {
this.classifier = classifier;
this.osDetectorClassifier = osDetectorClassifier;
}
public String getClassifier() {
return classifier;
}
public static JavaFXPlatform detect(Project project) {
String osClassifier = project.getExtensions().getByType(OsDetector.class).getClassifier();
if("true".equals(System.getProperty("java.awt.headless"))) {
osClassifier += "-monocle";
}
for ( JavaFXPlatform platform: values()) {
if ( platform.osDetectorClassifier.equals(osClassifier)) {
return platform;
}
}
String supportedPlatforms = Arrays.stream(values())
.map(p->p.osDetectorClassifier)
.collect(Collectors.joining("', '", "'", "'"));
throw new GradleException(
String.format(
"Unsupported JavaFX platform found: '%s'! " +
"This plugin is designed to work on supported platforms only." +
"Current supported platforms are %s.", osClassifier, supportedPlatforms )
);
}
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2018, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openjfx.gradle;
import com.google.gradle.osdetector.OsDetectorPlugin;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.javamodularity.moduleplugin.ModuleSystemPlugin;
import org.openjfx.gradle.tasks.ExecTask;
public class JavaFXPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPlugins().apply(OsDetectorPlugin.class);
project.getPlugins().apply(ModuleSystemPlugin.class);
project.getExtensions().create("javafx", JavaFXOptions.class, project);
project.getTasks().create("configJavafxRun", ExecTask.class, project);
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright (c) 2019, 2021, Gluon
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openjfx.gradle.tasks;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.Project;
import org.gradle.api.file.FileCollection;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.plugins.ApplicationPlugin;
import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.TaskAction;
import org.javamodularity.moduleplugin.extensions.RunModuleOptions;
import org.openjfx.gradle.JavaFXModule;
import org.openjfx.gradle.JavaFXOptions;
import org.openjfx.gradle.JavaFXPlatform;
import javax.inject.Inject;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.TreeSet;
public class ExecTask extends DefaultTask {
private static final Logger LOGGER = Logging.getLogger(ExecTask.class);
private final Project project;
private JavaExec execTask;
@Inject
public ExecTask(Project project) {
this.project = project;
project.getPluginManager().withPlugin(ApplicationPlugin.APPLICATION_PLUGIN_NAME, e -> {
execTask = (JavaExec) project.getTasks().findByName(ApplicationPlugin.TASK_RUN_NAME);
if (execTask != null) {
execTask.dependsOn(this);
} else {
throw new GradleException("Run task not found.");
}
});
}
@TaskAction
public void action() {
if (execTask != null) {
JavaFXOptions javaFXOptions = project.getExtensions().getByType(JavaFXOptions.class);
JavaFXModule.validateModules(javaFXOptions.getModules());
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
if (!definedJavaFXModuleNames.isEmpty()) {
RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class);
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
);
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
if (moduleOptions != null) {
LOGGER.info("Modular JavaFX application found");
// Remove empty JavaFX jars from classpath
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
definedJavaFXModuleNames.forEach(javaFXModule -> moduleOptions.getAddModules().add(javaFXModule));
} else {
LOGGER.info("Non-modular JavaFX application found");
// Remove all JavaFX jars from classpath
execTask.setClasspath(classpathWithoutJavaFXJars);
var javaFXModuleJvmArgs = List.of("--module-path", javaFXPlatformJars.getAsPath());
var jvmArgs = new ArrayList<String>();
jvmArgs.add("--add-modules");
jvmArgs.add(String.join(",", definedJavaFXModuleNames));
List<String> execJvmArgs = execTask.getJvmArgs();
if (execJvmArgs != null) {
jvmArgs.addAll(execJvmArgs);
}
jvmArgs.addAll(javaFXModuleJvmArgs);
execTask.setJvmArgs(jvmArgs);
}
}
} else {
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
}
}
private static boolean isJavaFXJar(File jar, JavaFXPlatform platform) {
return jar.isFile() &&
Arrays.stream(JavaFXModule.values()).anyMatch(javaFXModule ->
javaFXModule.compareJarFileName(platform, jar.getName()) ||
javaFXModule.getModuleJarFileName().equals(jar.getName()));
}
}

View file

@ -82,7 +82,7 @@ sudo apt install -y rpm fakeroot binutils
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
```shell
GIT_TAG="1.8.2"
GIT_TAG="1.8.4"
```
The project can then be initially cloned as follows:

View file

@ -1,3 +1,3 @@
mime-type=application/pgp-signature
extension=asc
description=ASCII Armored Signature
description=ASCII Armored File

View file

@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.8.3</string>
<string>1.8.5</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@ -105,7 +105,7 @@
</array>
</dict>
<key>UTTypeDescription</key>
<string>ASCII Armored Signature</string>
<string>ASCII Armored File</string>
<key>UTTypeIconFile</key>
<string>sparrow.icns</string>
</dict>

View file

@ -21,6 +21,8 @@ import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
@ -289,9 +291,7 @@ public class AppController implements Initializable {
Dragboard db = event.getDragboard();
boolean success = false;
if(db.hasFiles()) {
for(File file : db.getFiles()) {
openFile(file);
}
openFiles(db.getFiles());
success = true;
}
event.setDropCompleted(success);
@ -333,6 +333,8 @@ public class AppController implements Initializable {
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyList()));
});
tabs.setPickOnBounds(false);
registerShortcuts();
BitcoinUnit unit = Config.get().getBitcoinUnit();
@ -379,6 +381,9 @@ public class AppController implements Initializable {
preventSleepProperty.set(Config.get().isPreventSleep());
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
MenuItem homeItem = new MenuItem("Home Folder...");
homeItem.setOnAction(this::restartInHome);
restart.getItems().add(homeItem);
List<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET));
networks.remove(Network.get());
for(Network network : networks) {
@ -636,7 +641,7 @@ public class AppController implements Initializable {
} catch(TransactionParseException e) {
showErrorDialog("Invalid transaction", e.getMessage());
} catch(Exception e) {
showErrorDialog("Invalid file", "Cannot recognise the format of this file.");
showErrorDialog("Invalid file", "Cannot recognise the format of the " + file.getName() + " file.");
}
}
}
@ -752,8 +757,10 @@ public class AppController implements Initializable {
Transaction transaction = transactionTabData.getTransaction();
try {
UR ur = UR.fromBytes(transaction.bitcoinSerialize());
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur);
byte[] txBytes = transaction.bitcoinSerialize();
UR ur = UR.fromBytes(txBytes);
BBQR bbqr = new BBQR(BBQRType.TXN, txBytes);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
qrDisplayDialog.initOwner(rootStack.getScene().getWindow());
qrDisplayDialog.showAndWait();
} catch(Exception e) {
@ -851,8 +858,10 @@ public class AppController implements Initializable {
if(tabData.getType() == TabData.TabType.TRANSACTION) {
TransactionTabData transactionTabData = (TransactionTabData)tabData;
CryptoPSBT cryptoPSBT = new CryptoPSBT(transactionTabData.getPsbt().serialize());
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR());
byte[] psbtBytes = transactionTabData.getPsbt().serialize();
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
qrDisplayDialog.initOwner(rootStack.getScene().getWindow());
qrDisplayDialog.show();
}
@ -967,19 +976,46 @@ public class AppController implements Initializable {
AppServices.get().setPreventSleep(item.isSelected());
}
public void restartInHome(ActionEvent event) {
Args args = getRestartArgs();
File initialDir = null;
if(args.dir != null) {
initialDir = new File(args.dir);
}
Stage window = new Stage();
DirectoryChooser directoryChooser = new DirectoryChooser();
directoryChooser.setTitle("Choose Sparrow Home Folder");
directoryChooser.setInitialDirectory(initialDir == null || !initialDir.exists() ? Storage.getSparrowHome() : initialDir);
File newHome = directoryChooser.showDialog(window);
if(newHome != null) {
args.dir = newHome.getAbsolutePath();
restart(event, args);
}
}
public void restart(ActionEvent event, Network network) {
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
}
Args args = getRestartArgs();
args.network = network;
restart(event, args);
}
private static Args getRestartArgs() {
Args args = new Args();
ProcessHandle.current().info().arguments().ifPresent(argv -> {
JCommander jCommander = JCommander.newBuilder().addObject(args).acceptUnknownOptions(true).build();
jCommander.parse(argv);
});
args.network = network;
return args;
}
private void restart(ActionEvent event, Args args) {
try {
List<String> cmd = new ArrayList<>();
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
@ -992,15 +1028,21 @@ public class AppController implements Initializable {
}
}
public void openFile(File file) {
public void openFiles(List<File> files) {
boolean verifyOpened = false;
for(File file : files) {
if(isWalletFile(file)) {
openWalletFile(file, true);
} else if(isVerifyDownloadFile(file)) {
if(!verifyOpened) {
verifyDownload(new ActionEvent(file, rootStack));
verifyOpened = true;
}
} else {
openTransactionFile(file);
}
}
}
private void setServerToggleTooltip(Integer currentBlockHeight) {
Tooltip tooltip = new Tooltip(getServerToggleTooltipText(currentBlockHeight));
@ -2743,7 +2785,7 @@ public class AppController implements Initializable {
public void disconnection(DisconnectionEvent event) {
serverToggle.setDisable(false);
if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith(CONNECTION_FAILED_PREFIX) && !statusBar.getText().contains(TRYING_ANOTHER_SERVER_MESSAGE)) {
statusUpdated(new StatusEvent("Disconnected"));
statusUpdated(new StatusEvent("Disconnected (click toggle on the right to connect)", 240));
}
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(0);

View file

@ -117,6 +117,8 @@ public class AppServices {
private ElectrumServer.ConnectionService connectionService;
private ElectrumServer.FeeRatesService feeRatesService;
private Hwi.ScheduledEnumerateService deviceEnumerateService;
private VersionCheckService versionCheckService;
@ -188,6 +190,7 @@ public class AppServices {
public void start() {
Config config = Config.get();
connectionService = createConnectionService();
feeRatesService = createFeeRatesService();
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
versionCheckService = createVersionCheckService();
torService = createTorService();
@ -201,6 +204,8 @@ public class AppServices {
} else {
restartServices();
}
} else {
EventManager.get().post(new DisconnectionEvent());
}
addURIHandlers();
@ -284,8 +289,15 @@ public class AppServices {
onlineProperty.setValue(true);
onlineProperty.addListener(onlineServicesListener);
if(connectionService.getValue() != null) {
EventManager.get().post(connectionService.getValue());
FeeRatesUpdatedEvent event = connectionService.getValue();
if(event != null) {
EventManager.get().post(event);
}
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
if(event instanceof ConnectionEvent && Network.get().equals(Network.MAINNET) && feeRatesSource.isExternal()) {
EventManager.get().post(new FeeRatesSourceChangedEvent(feeRatesSource));
}
});
connectionService.setOnFailed(failEvent -> {
@ -356,6 +368,15 @@ public class AppServices {
return connectionService;
}
private ElectrumServer.FeeRatesService createFeeRatesService() {
ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService();
feeRatesService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(feeRatesService.getValue());
});
return feeRatesService;
}
private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) {
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
@ -1102,18 +1123,19 @@ public class AppServices {
@Subscribe
public void mempoolRateSizes(MempoolRateSizesUpdatedEvent event) {
if(event.getMempoolRateSizes() != null) {
addMempoolRateSizes(event.getMempoolRateSizes());
}
}
@Subscribe
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService();
feeRatesService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(feeRatesService.getValue());
});
//Perform once-off fee rates retrieval to immediately change displayed rates
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
feeRatesService = createFeeRatesService();
feeRatesService.start();
}
}
@Subscribe
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {

View file

@ -16,9 +16,9 @@ import java.io.File;
import java.util.*;
public class SparrowWallet {
public static final String APP_ID = "com.sparrowwallet.sparrow";
public static final String APP_ID = "sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "1.8.3";
public static final String APP_VERSION = "1.8.5";
public static final String APP_VERSION_SUFFIX = "";
public static final String APP_HOME_PROPERTY = "sparrow.home";
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
@ -79,7 +79,7 @@ public class SparrowWallet {
try {
instance = new Instance(fileUriArguments);
instance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
instance.acquireLock(!fileUriArguments.isEmpty()); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
} catch(InstanceException e) {
getLogger().error("Could not access application lock", e);
}
@ -130,13 +130,13 @@ public class SparrowWallet {
private final List<String> fileUriArguments;
public Instance(List<String> fileUriArguments) {
super(SparrowWallet.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
super(SparrowWallet.APP_ID, true);
this.fileUriArguments = fileUriArguments;
}
@Override
protected void receiveMessageList(List<String> messageList) {
if(messageList != null && !messageList.isEmpty()) {
if(messageList != null) {
AppServices.parseFileUriArguments(messageList);
AppServices.openFileUriArguments(null);
}

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.pgp.PGPKeySource;
import com.sparrowwallet.drongo.pgp.PGPUtils;
import com.sparrowwallet.drongo.pgp.PGPVerificationException;
import com.sparrowwallet.drongo.pgp.PGPVerificationResult;
@ -70,6 +71,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>();
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
private final BooleanProperty manifestDisabled = new SimpleBooleanProperty();
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
private final Label signedBy;
@ -99,7 +101,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
String version = VersionCheckService.getVersion() != null ? VersionCheckService.getVersion() : "x.x.x";
Field signatureField = setupField(signature, "Signature", SIGNATURE_EXTENSIONS, false, "sparrow-" + version + "-manifest.txt", null);
Field manifestField = setupField(manifest, "Manifest", MANIFEST_EXTENSIONS, false, "sparrow-" + version + "-manifest", null);
Field manifestField = setupField(manifest, "Manifest", MANIFEST_EXTENSIONS, false, "sparrow-" + version + "-manifest", manifestDisabled);
Field publicKeyField = setupField(publicKey, "Public Key", PUBLIC_KEY_EXTENSIONS, true, "pgp_keys", publicKeyDisabled);
Field releaseFileField = setupField(release, "Release File", getReleaseFileExtensions(), false, getReleaseFileExample(version), null);
@ -153,6 +155,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
release.set(null);
signedBy.setText("");
signedBy.setGraphic(null);
signedBy.setTooltip(null);
releaseHash.setText("");
releaseHash.setGraphic(null);
releaseVerified.setText("");
@ -262,6 +265,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
private void verify() {
manifestDisabled.set(false);
publicKeyDisabled.set(false);
if(signature.get() == null || manifest.get() == null) {
@ -282,12 +286,14 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
String message = result.userId() + " on " + signatureDateFormat.format(result.signatureTimestamp()) + (result.expired() ? " (key expired)" : "");
signedBy.setText(message);
signedBy.setGraphic(result.expired() ? GlyphUtils.getWarningGlyph() : GlyphUtils.getSuccessGlyph());
signedBy.setTooltip(new Tooltip(result.fingerprint()));
if(!result.expired()) {
if(!result.expired() && result.keySource() != PGPKeySource.USER) {
publicKeyDisabled.set(true);
}
if(manifest.get().equals(release.get())) {
manifestDisabled.set(true);
releaseHash.setText("No hash required, signature signs release file directly");
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
releaseHash.setTooltip(null);
@ -302,6 +308,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
Throwable e = event.getSource().getException();
signedBy.setText(getDisplayMessage(e));
signedBy.setGraphic(GlyphUtils.getFailureGlyph());
signedBy.setTooltip(null);
clearReleaseFields();
});

View file

@ -585,12 +585,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
getItems().add(createCpfp);
}
if(!Config.get().isBlockExplorerDisabled()) {
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
openBlockExplorer.setOnAction(AE -> {
hide();
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
});
getItems().add(openBlockExplorer);
}
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
copyTxid.setOnAction(AE -> {
@ -612,12 +614,16 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
hide();
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
});
getItems().add(viewTransaction);
if(!Config.get().isBlockExplorerDisabled()) {
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
openBlockExplorer.setOnAction(AE -> {
hide();
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
});
getItems().add(openBlockExplorer);
}
MenuItem copyDate = new MenuItem("Copy Date");
copyDate.setOnAction(AE -> {
@ -626,6 +632,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
content.putString(date);
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyDate);
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
copyTxid.setOnAction(AE -> {
@ -634,6 +641,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
content.putString(blockTransaction.getHashAsString());
Clipboard.getSystemClipboard().setContent(content);
});
getItems().add(copyTxid);
MenuItem copyHeight = new MenuItem("Copy Block Height");
copyHeight.setOnAction(AE -> {
@ -642,8 +650,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
content.putString(blockTransaction.getHeight() > 0 ? Integer.toString(blockTransaction.getHeight()) : "Mempool");
Clipboard.getSystemClipboard().setContent(content);
});
getItems().addAll(viewTransaction, openBlockExplorer, copyDate, copyTxid, copyHeight);
getItems().add(copyHeight);
}
}

View file

@ -4,12 +4,14 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
@ -153,7 +155,9 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
} else {
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof Bip129) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), baos.toByteArray(), false);
UR ur = UR.fromBytes(baos.toByteArray());
BBQR bbqr = new BBQR(BBQRType.UNICODE, baos.toByteArray());
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
} else {
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
}

View file

@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
@ -13,6 +14,8 @@ import com.sparrowwallet.sparrow.event.TimedEvent;
import com.sparrowwallet.sparrow.event.WalletExportEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.geometry.Pos;
@ -163,8 +166,12 @@ public class FileWalletExportPane extends TitledDescriptionPane {
QRDisplayDialog qrDisplayDialog;
if(exporter instanceof CoboVaultMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig || exporter instanceof Bip129) {
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
} else if(exporter instanceof Bip129) {
UR ur = UR.fromBytes(outputStream.toByteArray());
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
} else if(exporter instanceof Descriptor) {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);

View file

@ -472,7 +472,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(qrText, true);
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.NEXT_FORWARD) {
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
scanQr();
}
}

View file

@ -32,6 +32,7 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder;
import com.sparrowwallet.sparrow.io.bbqr.BBQRException;
import com.sparrowwallet.sparrow.wallet.KeystoreController;
import javafx.application.Platform;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
@ -621,7 +622,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
List<ChildNumber> path = cryptoKeypath.getComponents().stream().map(comp -> (IndexPathComponent)comp)
.map(comp -> new ChildNumber(comp.getIndex(), comp.isHardened())).collect(Collectors.toList());
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), KeyDerivation.writePath(path));
String fingerprint = cryptoKeypath.getSourceFingerprint() == null ? KeystoreController.DEFAULT_WATCH_ONLY_FINGERPRINT : Utils.bytesToHex(cryptoKeypath.getSourceFingerprint());
return new KeyDerivation(fingerprint, KeyDerivation.writePath(path));
}
return null;

View file

@ -54,10 +54,10 @@ public class ScriptArea extends CodeArea {
ScriptChunk chunk = script.getChunks().get(i);
if(chunk.isOpCode()) {
append(chunk.toString(), "script-opcode");
} else if(chunk.isSignature()) {
append("<signature" + signatureCount++ + ">", "script-signature");
} else if(chunk.isPubKey()) {
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
} else if(chunk.isSignature()) {
append("<signature" + signatureCount++ + ">", "script-signature");
} else if(chunk.isTaprootControlBlock()) {
append("<controlblock>", "script-controlblock");
} else if(chunk.isString()) {

View file

@ -1,487 +1,225 @@
/**
* Copyright 2019 Pratanu Mandal
*
* 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 com.sparrowwallet.sparrow.instance;
import com.sparrowwallet.sparrow.io.Storage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.ConnectException;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.net.StandardProtocolFamily;
import java.net.UnixDomainSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.Set;
/**
* The <code>Instance</code> class is the primary logical entry point to the library.<br>
* It allows to create an application lock or free it and send and receive messages between first and subsequent instances.<br><br>
*
* <pre>
* // unique application ID
* String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";
*
* // create Instance instance
* Instance unique = new Instance(APP_ID) {
* &nbsp;&nbsp;&nbsp;&nbsp;&#64;Override
* &nbsp;&nbsp;&nbsp;&nbsp;protected void receiveMessage(String message) {
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// print received message (timestamp)
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(message);
* &nbsp;&nbsp;&nbsp;&nbsp;}
* &nbsp;&nbsp;&nbsp;&nbsp;
* &nbsp;&nbsp;&nbsp;&nbsp;&#64;Override
* &nbsp;&nbsp;&nbsp;&nbsp;protected String sendMessage() {
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;// send timestamp as message
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Timestamp ts = new Timestamp(new Date().getTime());
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return "Another instance launch attempted: " + ts.toString();
* &nbsp;&nbsp;&nbsp;&nbsp;}
* };
*
* // try to obtain lock
* try {
* &nbsp;&nbsp;&nbsp;&nbsp;unique.acquireLock();
* } catch (InstanceException e) {
* &nbsp;&nbsp;&nbsp;&nbsp;e.printStackTrace();
* }
*
* ...
*
* // try to free the lock before exiting program
* try {
* &nbsp;&nbsp;&nbsp;&nbsp;unique.freeLock();
* } catch (InstanceException e) {
* &nbsp;&nbsp;&nbsp;&nbsp;e.printStackTrace();
* }
* </pre>
*
* @author Pratanu Mandal
* @since 1.3
*
*/
public abstract class Instance {
private static final Logger log = LoggerFactory.getLogger(Instance.class);
private static final String LINK_ENV_PROPERTY = "SPARROW_NO_LOCK_FILE_LINK";
// starting position of port check
private static final int PORT_START = 7221;
public final String applicationId;
private final boolean autoExit;
// system temporary directory path
private static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
private Selector selector;
private ServerSocketChannel serverChannel;
/**
* Unique string representing the application ID.<br><br>
*
* The APP_ID must be as unique as possible.
* Avoid generic names like "my_app_id" or "hello_world".<br>
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
*/
public final String APP_ID;
// auto exit from application or not
private final boolean AUTO_EXIT;
// lock server port
private int port;
// lock server socket
private ServerSocket server;
// lock file RAF object
private RandomAccessFile lockRAF;
// file lock for the lock file RAF object
private FileLock fileLock;
/**
* Parameterized constructor.<br>
* This constructor configures to automatically exit the application for subsequent instances.<br><br>
*
* The APP_ID must be as unique as possible.
* Avoid generic names like "my_app_id" or "hello_world".<br>
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
*
* @param APP_ID Unique string representing the application ID
*/
public Instance(final String APP_ID) {
this(APP_ID, true);
public Instance(final String applicationId) {
this(applicationId, true);
}
/**
* Parameterized constructor.<br>
* This constructor allows to explicitly specify the exit strategy for subsequent instances.<br><br>
*
* The APP_ID must be as unique as possible.
* Avoid generic names like "my_app_id" or "hello_world".<br>
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
*
* @since 1.2
*
* @param APP_ID Unique string representing the application ID
* @param AUTO_EXIT If true, automatically exit the application for subsequent instances
*/
public Instance(final String APP_ID, final boolean AUTO_EXIT) {
this.APP_ID = APP_ID;
this.AUTO_EXIT = AUTO_EXIT;
public Instance(final String applicationId, final boolean autoExit) {
this.applicationId = applicationId;
this.autoExit = autoExit;
}
/**
* Try to obtain lock. If not possible, send data to first instance.
*
* @deprecated Use <code>acquireLock()</code> instead.
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
*/
@Deprecated
public void lock() throws InstanceException {
acquireLock();
public void acquireLock(boolean findExisting) throws InstanceException {
Path lockFile = getLockFile(findExisting);
if(!Files.exists(lockFile)) {
startServer(lockFile);
} else {
doClient(lockFile);
}
}
/**
* Try to obtain lock. If not possible, send data to first instance.
*
* @since 1.2
*
* @return true if able to acquire lock, false otherwise
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
*/
public boolean acquireLock() throws InstanceException {
// try to obtain port number from lock file
port = lockFile();
if (port == -1) {
// failed to fetch port number
// try to start server
startServer();
}
else {
// port number fetched from lock file
// try to start client
doClient();
private void startServer(Path lockFile) throws InstanceException {
try {
selector = Selector.open();
UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(lockFile);
serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX);
serverChannel.bind(socketAddress);
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
lockFile.toFile().deleteOnExit();
} catch(Exception e) {
throw new InstanceException("Could not open UNIX socket lock file for instance at " + lockFile.toAbsolutePath(), e);
}
return (server != null);
}
// start the server
private void startServer() throws InstanceException {
// try to create server
port = PORT_START;
Thread thread = new Thread(() -> {
while(true) {
try {
server = new ServerSocket(port, 50, InetAddress.getByName(null));
break;
} catch (IOException e) {
port++;
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectedKeys.iterator();
while(iter.hasNext()) {
SelectionKey key = iter.next();
if(key.isAcceptable()) {
SocketChannel client = serverChannel.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
}
}
// try to lock file
lockFile(port);
// server created successfully; this is the first instance
// keep listening for data from other instances
Thread thread = new Thread() {
@Override
public void run() {
while (!server.isClosed()) {
try {
// establish connection
final Socket socket = server.accept();
// handle socket on a different thread to allow parallel connections
Thread thread = new Thread() {
@Override
public void run() {
try {
// open writer
OutputStream os = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
// open reader
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
// read message length from client
int length = dis.readInt();
// read message string from client
String message = null;
if (length > -1) {
byte[] messageBytes = new byte[length];
int bytesRead = dis.read(messageBytes, 0, length);
message = new String(messageBytes, 0, bytesRead, "UTF-8");
}
// write response to client
if (APP_ID == null) {
dos.writeInt(-1);
}
else {
byte[] appId = APP_ID.getBytes("UTF-8");
dos.writeInt(appId.length);
dos.write(appId);
}
dos.flush();
// close writer and reader
dos.close();
dis.close();
// perform user action on message
if(key.isReadable()) {
try(SocketChannel clientChannel = (SocketChannel)key.channel()) {
String message = readMessage(clientChannel);
clientChannel.write(ByteBuffer.wrap(applicationId.getBytes(StandardCharsets.UTF_8)));
receiveMessage(message);
// close socket
socket.close();
} catch (IOException e) {
handleException(new InstanceException(e));
}
}
};
// start socket thread
thread.start();
iter.remove();
}
} catch(SocketException e) {
if (!server.isClosed()) {
if(serverChannel.isOpen()) {
handleException(new InstanceException(e));
}
} catch (IOException e) {
} catch(Exception e) {
handleException(new InstanceException(e));
}
}
}
};
});
thread.setDaemon(true);
thread.setName("SparrowInstanceListener");
thread.start();
createSymlink(lockFile);
}
// do client tasks
private void doClient() throws InstanceException {
// get localhost address
InetAddress address = null;
try {
address = InetAddress.getByName(null);
} catch (UnknownHostException e) {
throw new InstanceException(e);
}
// try to establish connection to server
Socket socket = null;
try {
socket = new Socket(address, port);
} catch (IOException e) {
// connection failed try to start server
startServer();
}
// connection successful try to connect to server
if (socket != null) {
try {
// get message to be sent to first instance
private void doClient(Path lockFile) throws InstanceException {
try(SocketChannel client = SocketChannel.open(UnixDomainSocketAddress.of(lockFile))) {
String message = sendMessage();
client.write(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8)));
client.shutdownOutput();
// open writer
OutputStream os = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(os);
// open reader
InputStream is = socket.getInputStream();
DataInputStream dis = new DataInputStream(is);
// write message to server
if (message == null) {
dos.writeInt(-1);
}
else {
byte[] messageBytes = message.getBytes("UTF-8");
dos.writeInt(messageBytes.length);
dos.write(messageBytes);
}
dos.flush();
// read response length from server
int length = dis.readInt();
// read response string from server
String response = null;
if (length > -1) {
byte[] responseBytes = new byte[length];
int bytesRead = dis.read(responseBytes, 0, length);
response = new String(responseBytes, 0, bytesRead, "UTF-8");
}
// close writer and reader
dos.close();
dis.close();
if (response.equals(APP_ID)) {
// validation successful
if (AUTO_EXIT) {
// perform pre-exit tasks
String response = readMessage(client);
if(response.equals(applicationId) && autoExit) {
beforeExit();
// exit this instance
System.exit(0);
}
}
else {
// validation failed, this is the first instance
startServer();
}
} catch (IOException e) {
throw new InstanceException(e);
} finally {
// close socket
} catch(ConnectException e) {
try {
if (socket != null) socket.close();
} catch (IOException e) {
throw new InstanceException(e);
}
Files.deleteIfExists(lockFile);
startServer(lockFile);
} catch(Exception ex) {
throw new InstanceException("Could not delete lock file from previous instance", e);
}
} catch(Exception e) {
throw new InstanceException("Could not open client connection to existing instance", e);
}
}
// try to get port from lock file
private int lockFile() throws InstanceException {
// lock file path
String filePath = TEMP_DIR + File.separator + APP_ID + ".lock";
File file = new File(filePath);
// try to get port from lock file
if (file.exists()) {
BufferedReader br = null;
try {
br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
return Integer.parseInt(br.readLine());
} catch (IOException e) {
throw new InstanceException(e);
} catch (NumberFormatException e) {
// do nothing
} finally {
try {
if (br != null) br.close();
} catch (IOException e) {
throw new InstanceException(e);
private static String readMessage(SocketChannel clientChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder messageBuilder = new StringBuilder();
while(clientChannel.read(buffer) != -1) {
buffer.flip();
messageBuilder.append(new String(buffer.array(), 0, buffer.limit()));
buffer.clear();
}
return messageBuilder.toString();
}
private Path getLockFile(boolean findExisting) {
if(findExisting) {
Path pointer = getUserLockFilePointer();
try {
if(pointer != null && Files.exists(pointer)) {
if(Files.isSymbolicLink(pointer)) {
return Files.readSymbolicLink(pointer);
} else {
Path lockFile = Path.of(Files.readString(pointer, StandardCharsets.UTF_8));
if(Files.exists(lockFile)) {
return lockFile;
}
}
}
} catch(IOException e) {
log.warn("Could not find lock file at " + pointer.toAbsolutePath());
} catch(Exception e) {
//ignore
}
}
return -1;
return Storage.getSparrowDir().toPath().resolve(applicationId + ".lock");
}
// try to write port to lock file
private void lockFile(int port) throws InstanceException {
// lock file path
String filePath = TEMP_DIR + File.separator + APP_ID + ".lock";
File file = new File(filePath);
private void createSymlink(Path lockFile) {
Path pointer = getUserLockFilePointer();
try {
if(pointer != null && !Files.exists(pointer, LinkOption.NOFOLLOW_LINKS)) {
Files.createSymbolicLink(pointer, lockFile);
pointer.toFile().deleteOnExit();
}
} catch(IOException e) {
log.debug("Could not create symlink " + pointer.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath() + ", writing as normal file", e);
// try to write port to lock file
BufferedWriter bw = null;
try {
bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));
bw.write(String.valueOf(port));
} catch (IOException e) {
throw new InstanceException(e);
} finally {
try {
if (bw != null) bw.close();
} catch (IOException e) {
throw new InstanceException(e);
Files.writeString(pointer, lockFile.toAbsolutePath().toString(), StandardCharsets.UTF_8);
pointer.toFile().deleteOnExit();
} catch(IOException ex) {
log.warn("Could not create pointer " + pointer.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath(), ex);
}
} catch(Exception e) {
//ignore
}
}
// try to obtain file lock
try {
lockRAF = new RandomAccessFile(file, "rw");
FileChannel fc = lockRAF.getChannel();
fileLock = fc.tryLock(0, Long.MAX_VALUE, true);
if (fileLock == null) {
throw new InstanceException("Failed to obtain file lock");
private Path getUserLockFilePointer() {
if(Boolean.parseBoolean(System.getenv(LINK_ENV_PROPERTY))) {
return null;
}
} catch (FileNotFoundException e) {
throw new InstanceException(e);
} catch (IOException e) {
throw new InstanceException(e);
try {
File sparrowHome = Storage.getSparrowHome(true);
if(!sparrowHome.exists()) {
Storage.createOwnerOnlyDirectory(sparrowHome);
}
return sparrowHome.toPath().resolve(applicationId + ".default");
} catch(Exception e) {
return null;
}
}
/**
* Free the lock if possible. This is only required to be called from the first instance.
*
* @deprecated Use <code>freeLock()</code> instead.
* @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock
*/
@Deprecated
public void free() throws InstanceException {
freeLock();
}
/**
* Free the lock if possible. This is only required to be called from the first instance.
*
* @since 1.2
*
* @return true if able to release lock, false otherwise
* @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock
*/
public boolean freeLock() throws InstanceException {
public void freeLock() throws InstanceException {
try {
// close server socket
if (server != null) {
server.close();
// lock file path
String filePath = TEMP_DIR + File.separator + APP_ID + ".lock";
File file = new File(filePath);
// try to release file lock
if (fileLock != null) {
fileLock.release();
if(serverChannel != null && serverChannel.isOpen()) {
serverChannel.close();
}
// try to close lock file RAF object
if (lockRAF != null) {
lockRAF.close();
if(getUserLockFilePointer() != null) {
Files.deleteIfExists(getUserLockFilePointer());
}
// try to delete lock file
if (file.exists()) {
file.delete();
}
return true;
}
return false;
} catch (IOException e) {
Files.deleteIfExists(getLockFile(false));
} catch(Exception e) {
throw new InstanceException(e);
}
}
@ -527,9 +265,6 @@ public abstract class Instance {
* This method is not invoked if auto exit is turned off.<br><br>
*
* This method is not synchronized.
*
* @since 1.2
*/
protected void beforeExit() {}
}

View file

@ -7,88 +7,14 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
/**
* The <code>InstanceList</code> class is a logical entry point to the library which extends the functionality of the <code>Instance</code> class.<br>
* It allows to create an application lock or free it and send and receive messages between first and subsequent instances.<br><br>
*
* This class is intended for passing a list of strings instead of a single string from the subsequent instance to the first instance.<br><br>
*
* <pre>
* // unique application ID
* String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";
*
* // create Instance instance
* Instance unique = new InstanceList(APP_ID) {
* &nbsp;&nbsp;&nbsp;&nbsp;&#64;Override
* &nbsp;&nbsp;&nbsp;&nbsp;protected List&lt;String&gt; sendMessageList() {
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;List&lt;String&gt; messageList = new ArrayList&lt;String&gt;();
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;messageList.add("Message 1");
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;messageList.add("Message 2");
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;messageList.add("Message 3");
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;messageList.add("Message 4");
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return messageList;
* &nbsp;&nbsp;&nbsp;&nbsp;}
*
* &nbsp;&nbsp;&nbsp;&nbsp;&#64;Override
* &nbsp;&nbsp;&nbsp;&nbsp;protected void receiveMessageList(List&lt;String&gt; messageList) {
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;for (String message : messageList) {
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;System.out.println(message);
* &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
* &nbsp;&nbsp;&nbsp;&nbsp;}
* };
*
* // try to obtain lock
* try {
* &nbsp;&nbsp;&nbsp;&nbsp;unique.acquireLock();
* } catch (InstanceException e) {
* &nbsp;&nbsp;&nbsp;&nbsp;e.printStackTrace();
* }
*
* ...
*
* // try to free the lock before exiting program
* try {
* &nbsp;&nbsp;&nbsp;&nbsp;unique.freeLock();
* } catch (InstanceException e) {
* &nbsp;&nbsp;&nbsp;&nbsp;e.printStackTrace();
* }
* </pre>
*
* @author Pratanu Mandal
* @since 1.3
*
*/
public abstract class InstanceList extends Instance {
/**
* Parameterized constructor.<br>
* This constructor configures to automatically exit the application for subsequent instances.<br><br>
*
* The APP_ID must be as unique as possible.
* Avoid generic names like "my_app_id" or "hello_world".<br>
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
*
* @param APP_ID Unique string representing the application ID
*/
public InstanceList(String APP_ID) {
super(APP_ID);
public InstanceList(String applicationId) {
super(applicationId);
}
/**
* Parameterized constructor.<br>
* This constructor allows to explicitly specify the exit strategy for subsequent instances.<br><br>
*
* The APP_ID must be as unique as possible.
* Avoid generic names like "my_app_id" or "hello_world".<br>
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
*
* @param APP_ID Unique string representing the application ID
* @param AUTO_EXIT If true, automatically exit the application for subsequent instances
*/
public InstanceList(String APP_ID, boolean AUTO_EXIT) {
super(APP_ID, AUTO_EXIT);
public InstanceList(String applicationId, boolean autoExit) {
super(applicationId, autoExit);
}
/**
@ -103,8 +29,7 @@ public abstract class InstanceList extends Instance {
protected final void receiveMessage(String message) {
if(message == null) {
receiveMessageList(null);
}
else {
} else {
// parse the JSON array string into an array of string arguments
JsonArray jsonArgs = JsonParser.parseString(message).getAsJsonArray();
@ -137,8 +62,9 @@ public abstract class InstanceList extends Instance {
JsonArray jsonArgs = new JsonArray();
List<String> stringArgs = sendMessageList();
if (stringArgs == null) return null;
if(stringArgs == null) {
return null;
}
for(String arg : stringArgs) {
jsonArgs.add(arg);
@ -168,5 +94,4 @@ public abstract class InstanceList extends Instance {
* @return list of messages sent from subsequent instances
*/
protected abstract List<String> sendMessageList();
}

View file

@ -535,7 +535,7 @@ public class Storage {
return certsDir;
}
static File getSparrowDir() {
public static File getSparrowDir() {
File sparrowDir;
if(Network.get() != Network.MAINNET) {
sparrowDir = new File(getSparrowHome(), Network.get().getName());
@ -551,7 +551,11 @@ public class Storage {
}
public static File getSparrowHome() {
if(System.getProperty(SparrowWallet.APP_HOME_PROPERTY) != null) {
return getSparrowHome(false);
}
public static File getSparrowHome(boolean useDefault) {
if(!useDefault && System.getProperty(SparrowWallet.APP_HOME_PROPERTY) != null) {
return new File(System.getProperty(SparrowWallet.APP_HOME_PROPERTY));
}

View file

@ -812,12 +812,18 @@ public class ElectrumServer {
return transactionMap;
}
public Map<Integer, Double> getFeeEstimates(List<Integer> targetBlocks) throws ServerException {
public Map<Integer, Double> getFeeEstimates(List<Integer> targetBlocks, boolean useCached) throws ServerException {
Map<Integer, Double> targetBlocksFeeRatesSats = getDefaultFeeEstimates(targetBlocks);
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
if(Network.get().equals(Network.MAINNET)) {
if(!feeRatesSource.isExternal()) {
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
} else if(useCached) {
if(AppServices.getTargetBlockFeeRates() != null) {
targetBlocksFeeRatesSats.putAll(AppServices.getTargetBlockFeeRates());
}
} else if(Network.get().equals(Network.MAINNET)) {
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
}
@ -1204,7 +1210,7 @@ public class ElectrumServer {
String banner = electrumServer.getServerBanner();
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE);
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, true);
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
feeRatesRetrievedAt = System.currentTimeMillis();
@ -1220,7 +1226,7 @@ public class ElectrumServer {
long elapsed = System.currentTimeMillis() - feeRatesRetrievedAt;
if(elapsed > FEE_RATES_PERIOD) {
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE);
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
feeRatesRetrievedAt = System.currentTimeMillis();
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
@ -1679,9 +1685,8 @@ public class ElectrumServer {
return new Task<>() {
protected FeeRatesUpdatedEvent call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer();
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE);
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
Map<Integer, Double> blockTargetFeeRates = electrumServer.getFeeEstimates(AppServices.TARGET_BLOCKS_RANGE, false);
return new FeeRatesUpdatedEvent(blockTargetFeeRates, null);
}
};
}

View file

@ -9,27 +9,27 @@ import java.util.LinkedHashMap;
import java.util.Map;
public enum FeeRatesSource {
ELECTRUM_SERVER("Server") {
ELECTRUM_SERVER("Server", false) {
@Override
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
return Collections.emptyMap();
}
},
MEMPOOL_SPACE("mempool.space") {
MEMPOOL_SPACE("mempool.space", true) {
@Override
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
String url = AppServices.isUsingProxy() ? "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/fees/recommended" : "https://mempool.space/api/v1/fees/recommended";
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
}
},
BITCOINFEES_EARN_COM("bitcoinfees.earn.com") {
BITCOINFEES_EARN_COM("bitcoinfees.earn.com", true) {
@Override
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended";
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
}
},
MINIMUM("Minimum (1 sat/vB)") {
MINIMUM("Minimum (1 sat/vB)", false) {
@Override
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
@ -40,7 +40,7 @@ public enum FeeRatesSource {
return blockTargetFeeRates;
}
},
OXT_ME("oxt.me") {
OXT_ME("oxt.me", true) {
@Override
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
String url = AppServices.isUsingProxy() ? "http://oxtwshnfyktikbflierkwcxxksbonl6v73l5so5zky7ur72w52tktkid.onion/stats/global/mempool" : "https://api.oxt.me/stats/global/mempool";
@ -63,9 +63,11 @@ public enum FeeRatesSource {
public static final int BLOCKS_IN_TWO_HOURS = 12;
private final String name;
private final boolean external;
FeeRatesSource(String name) {
FeeRatesSource(String name, boolean external) {
this.name = name;
this.external = external;
}
public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
@ -74,6 +76,10 @@ public enum FeeRatesSource {
return name;
}
public boolean isExternal() {
return external;
}
private static Map<Integer, Double> getThreeTierFeeRates(FeeRatesSource feeRatesSource, Map<Integer, Double> defaultblockTargetFeeRates, String url) {
if(log.isInfoEnabled()) {
log.info("Requesting fee rates from " + url);

View file

@ -1,16 +1,23 @@
package com.sparrowwallet.sparrow.net;
import com.google.common.net.HostAndPort;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.sparrow.AppServices;
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.*;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.file.Files;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TorUtils {
private static final Logger log = LoggerFactory.getLogger(TorUtils.class);
private static final Pattern TOR_OK = Pattern.compile("^2\\d{2}[ -]OK$");
private static final Pattern TOR_AUTH_METHODS = Pattern.compile("^2\\d{2}[ -]AUTH METHODS=(\\S+)\\s?(COOKIEFILE=\"?(.+?)\"?)?$");
public static void changeIdentity(HostAndPort proxy) {
if(AppServices.isTorRunning()) {
@ -22,16 +29,62 @@ public class TorUtils {
} else {
HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1);
try(Socket socket = new Socket(control.getHost(), control.getPort())) {
socket.setSoTimeout(1500);
if(authenticate(socket)) {
writeNewNym(socket);
}
} catch(TorAuthenticationException e) {
log.warn("Error authenticating to Tor at " + control + ", server returned " + e.getMessage());
} catch(SocketTimeoutException e) {
log.warn("Timeout reading from " + control + ", is this a Tor ControlPort?");
} catch(Exception e) {
log.warn("Error connecting to " + control + ", no Tor ControlPort configured?");
}
}
}
private static boolean authenticate(Socket socket) throws IOException, TorAuthenticationException {
socket.getOutputStream().write("PROTOCOLINFO\r\n".getBytes());
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line;
File cookieFile = null;
while((line = reader.readLine()) != null) {
Matcher authMatcher = TOR_AUTH_METHODS.matcher(line);
if(authMatcher.matches()) {
String methods = authMatcher.group(1);
if(methods.contains("COOKIE") && !authMatcher.group(3).isEmpty()) {
cookieFile = new File(authMatcher.group(3));
}
}
if(TOR_OK.matcher(line).matches()) {
break;
}
}
if(cookieFile != null && cookieFile.exists()) {
byte[] cookieBytes = Files.readAllBytes(cookieFile.toPath());
String authentication = "AUTHENTICATE " + Utils.bytesToHex(cookieBytes) + "\r\n";
socket.getOutputStream().write(authentication.getBytes());
} else {
socket.getOutputStream().write("AUTHENTICATE \"\"\r\n".getBytes());
}
line = reader.readLine();
if(TOR_OK.matcher(line).matches()) {
return true;
} else {
throw new TorAuthenticationException(line);
}
}
private static void writeNewNym(Socket socket) throws IOException {
log.debug("Sending NEWNYM to " + socket);
socket.getOutputStream().write("AUTHENTICATE \"\"\r\n".getBytes());
socket.getOutputStream().write("SIGNAL NEWNYM\r\n".getBytes());
}
private static class TorAuthenticationException extends Exception {
public TorAuthenticationException(String message) {
super(message);
}
}
}

View file

@ -906,7 +906,7 @@ public class HeadersController extends TransactionFormController implements Init
//TODO: Remove once Cobo Vault support has been removed
boolean addLegacyEncodingOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COBO_VAULT));
boolean addBbqrOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COLDCARD) || keystore.getSource().equals(KeystoreSource.SW_WATCH));
boolean addBbqrOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COLDCARD) || keystore.getSource().equals(KeystoreSource.SW_WATCH) || keystore.getSource().equals(KeystoreSource.SW_SEED));
boolean selectBbqrOption = headersForm.getSigningWallet().getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COLDCARD));
//Don't include non witness utxo fields for segwit wallets when displaying the PSBT as a QR - it can add greatly to the time required for scanning

View file

@ -16,6 +16,21 @@
-fx-font-size: 20;
}
.background-link {
-fx-padding: 0;
-fx-border-width: 0;
}
.background-link:visited,
.background-link:hover:armed {
-fx-text-fill: -fx-accent;
-fx-underline: false;
}
.background-link:hover:visited {
-fx-underline: true;
}
.drag-over > .background-text {
-fx-fill: #383a42;
}

View file

@ -143,7 +143,7 @@
<MenuItem mnemonicParsing="false" text="Verify Download" onAction="#verifyDownload" />
<MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/>
<CheckMenuItem fx:id="preventSleep" mnemonicParsing="false" text="Prevent Computer Sleep" onAction="#preventSleep"/>
<Menu fx:id="restart" mnemonicParsing="false" text="Restart In Network" />
<Menu fx:id="restart" mnemonicParsing="false" text="Restart In" />
</Menu>
<Menu fx:id="helpMenu" mnemonicParsing="false" text="Help">
<MenuItem mnemonicParsing="false" text="Show Introduction" onAction="#showIntroduction"/>
@ -156,12 +156,13 @@
</menus>
</MenuBar>
<DecorationPane fx:id="rootStack" VBox.vgrow="ALWAYS">
<Rectangle styleClass="background-box" width="450" height="170" />
<Rectangle styleClass="background-box" width="450" height="230" />
<HBox alignment="CENTER">
<VBox alignment="CENTER_LEFT" spacing="15">
<Text styleClass="background-text" text="File menu → New Wallet or" />
<Text styleClass="background-text" text="File menu → Import Wallet or" />
<Text styleClass="background-text" text="Drag files here to open" />
<VBox alignment="CENTER" spacing="15">
<HBox><Text styleClass="background-text" text="File menu → "/><Hyperlink onAction="#newWallet" styleClass="background-text,background-link" text="New Wallet"/><Text styleClass="background-text" text=" or"/></HBox>
<HBox><Text styleClass="background-text" text="File menu → "/><Hyperlink onAction="#openWallet" styleClass="background-text,background-link" text="Open Wallet"/><Text styleClass="background-text" text=" or"/></HBox>
<HBox><Text styleClass="background-text" text="File menu → "/><Hyperlink onAction="#importWallet" styleClass="background-text,background-link" text="Import Wallet"/><Text styleClass="background-text" text=" or"/></HBox>
<Text styleClass="background-text" text="drag files to open" />
</VBox>
</HBox>
<TabPane fx:id="tabs" />

View file

@ -156,11 +156,25 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
-fx-border-color: #626367;
}
.root .wallet-subtabs > .tab-header-area .tab {
.root .tab-pane > .tab-header-area > .headers-region {
-fx-color: derive(-fx-base, 40%);
-fx-mark-color: ladder(-fx-base, white 30%, derive(-fx-base,-63%) 31%);
}
.root .tab-pane > .tab-header-area > .headers-region > .tab .tab-label .label {
-fx-text-fill: derive(white, -8%);
}
.root .tab-pane > .tab-header-area > .headers-region > .tab:selected .tab-label .label,
.root .tab-pane > .tab-header-area > .headers-region > .tab:hover .tab-label .label {
-fx-text-fill: white;
}
.root .wallet-subtabs > .tab-header-area > .headers-region > .tab {
-fx-background-color: derive(#2284bb, 32%);
}
.root .wallet-subtabs > .tab-header-area .tab:selected {
.root .wallet-subtabs > .tab-header-area > .headers-region > .tab:selected {
-fx-background-color: #2284bb;
}

View file

@ -7,6 +7,7 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.MnemonicException;
import com.sparrowwallet.drongo.wallet.Wallet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@ -15,6 +16,7 @@ import java.io.*;
public class StorageTest extends IoTest {
@Test
public void loadWallet() throws IOException, MnemonicException, StorageException {
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, "true");
Storage storage = new Storage(getFile("sparrow-single-wallet"));
Wallet wallet = storage.loadEncryptedWallet("pass").getWallet();
Assertions.assertTrue(wallet.isValid());
@ -64,6 +66,7 @@ public class StorageTest extends IoTest {
@Test
public void saveWallet() throws IOException, MnemonicException, StorageException {
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, "true");
Storage storage = new Storage(getFile("sparrow-single-wallet"));
Wallet wallet = storage.loadEncryptedWallet("pass").getWallet();
Assertions.assertTrue(wallet.isValid());
@ -80,4 +83,9 @@ public class StorageTest extends IoTest {
wallet = temp2Storage.loadEncryptedWallet("pass").getWallet();
Assertions.assertTrue(wallet.isValid());
}
@AfterEach
void tearDown() {
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, "false");
}
}

View file

@ -2,7 +2,7 @@
#
Name: CC-2-of-4
Policy: 2 of 4
Derivation: m/48'/1'/0'/2'
Derivation: m/48'/0'/0'/2'
Format: P2WSH
0F056943: xpub6EfEGa5isJbQFSswM5Uptw5BSq2Td1ZDJr3QUNUcMySpC7itZ3ccypVHtLPnvMzKQ2qxrAgH49vhVxRcaQLFbixAVRR8RACrYTp88Uv9h8Z