mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-11-02 20:36:44 +00:00
Merge branch 'master' into whirlpool-1.0.0
This commit is contained in:
commit
8e66db0237
33 changed files with 1102 additions and 707 deletions
20
build.gradle
20
build.gradle
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
114
buildSrc/src/main/java/org/openjfx/gradle/JavaFXModule.java
Normal file
114
buildSrc/src/main/java/org/openjfx/gradle/JavaFXModule.java
Normal 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;
|
||||
}
|
||||
}
|
164
buildSrc/src/main/java/org/openjfx/gradle/JavaFXOptions.java
Normal file
164
buildSrc/src/main/java/org/openjfx/gradle/JavaFXOptions.java
Normal 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()));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 )
|
||||
);
|
||||
|
||||
}
|
||||
}
|
49
buildSrc/src/main/java/org/openjfx/gradle/JavaFXPlugin.java
Normal file
49
buildSrc/src/main/java/org/openjfx/gradle/JavaFXPlugin.java
Normal 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);
|
||||
}
|
||||
}
|
124
buildSrc/src/main/java/org/openjfx/gradle/tasks/ExecTask.java
Normal file
124
buildSrc/src/main/java/org/openjfx/gradle/tasks/ExecTask.java
Normal 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()));
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
mime-type=application/pgp-signature
|
||||
extension=asc
|
||||
description=ASCII Armored Signature
|
||||
description=ASCII Armored 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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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) {
|
||||
* @Override
|
||||
* protected void receiveMessage(String message) {
|
||||
* // print received message (timestamp)
|
||||
* System.out.println(message);
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected String sendMessage() {
|
||||
* // send timestamp as message
|
||||
* Timestamp ts = new Timestamp(new Date().getTime());
|
||||
* return "Another instance launch attempted: " + ts.toString();
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* // try to obtain lock
|
||||
* try {
|
||||
* unique.acquireLock();
|
||||
* } catch (InstanceException e) {
|
||||
* e.printStackTrace();
|
||||
* }
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* // try to free the lock before exiting program
|
||||
* try {
|
||||
* unique.freeLock();
|
||||
* } catch (InstanceException e) {
|
||||
* 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() {}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
* @Override
|
||||
* protected List<String> sendMessageList() {
|
||||
* List<String> messageList = new ArrayList<String>();
|
||||
*
|
||||
* messageList.add("Message 1");
|
||||
* messageList.add("Message 2");
|
||||
* messageList.add("Message 3");
|
||||
* messageList.add("Message 4");
|
||||
*
|
||||
* return messageList;
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected void receiveMessageList(List<String> messageList) {
|
||||
* for (String message : messageList) {
|
||||
* System.out.println(message);
|
||||
* }
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* // try to obtain lock
|
||||
* try {
|
||||
* unique.acquireLock();
|
||||
* } catch (InstanceException e) {
|
||||
* e.printStackTrace();
|
||||
* }
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* // try to free the lock before exiting program
|
||||
* try {
|
||||
* unique.freeLock();
|
||||
* } catch (InstanceException e) {
|
||||
* 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();
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue