mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2024-12-25 13:16: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 {
|
plugins {
|
||||||
id 'application'
|
id 'application'
|
||||||
id 'extra-java-module-info'
|
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'
|
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 os = org.gradle.internal.os.OperatingSystem.current()
|
||||||
def osName = os.getFamilyName()
|
def osName = os.getFamilyName()
|
||||||
if(os.macOsX) {
|
if(os.macOsX) {
|
||||||
|
@ -161,7 +161,7 @@ processResources {
|
||||||
|
|
||||||
test {
|
test {
|
||||||
useJUnitPlatform()
|
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 {
|
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.annotation",
|
||||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
|
"--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=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"]
|
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
|
||||||
|
|
||||||
if(os.windows) {
|
if(os.windows) {
|
||||||
|
@ -650,18 +652,6 @@ extraJavaModuleInfo {
|
||||||
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
|
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
|
||||||
exports('com.beust.jcommander')
|
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') {
|
module('jzlib-1.1.3.jar', 'com.jcraft.jzlib', '1.1.3') {
|
||||||
exports('com.jcraft.jzlib')
|
exports('com.jcraft.jzlib')
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ plugins {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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'
|
implementation 'org.ow2.asm:asm:9.6'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,5 +22,9 @@ gradlePlugin {
|
||||||
id = "extra-java-module-info"
|
id = "extra-java-module-info"
|
||||||
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
|
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 {
|
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
|
||||||
JarEntry jarEntry = inputStream.getNextJarEntry();
|
JarEntry jarEntry = inputStream.getNextJarEntry();
|
||||||
while (jarEntry != null) {
|
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.putNextEntry(jarEntry);
|
||||||
outputStream.write(inputStream.readAllBytes());
|
outputStream.write(inputStream.readAllBytes());
|
||||||
outputStream.closeEntry();
|
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:
|
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
GIT_TAG="1.8.2"
|
GIT_TAG="1.8.4"
|
||||||
```
|
```
|
||||||
|
|
||||||
The project can then be initially cloned as follows:
|
The project can then be initially cloned as follows:
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
mime-type=application/pgp-signature
|
mime-type=application/pgp-signature
|
||||||
extension=asc
|
extension=asc
|
||||||
description=ASCII Armored Signature
|
description=ASCII Armored File
|
|
@ -21,7 +21,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.8.3</string>
|
<string>1.8.5</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<key>UTTypeDescription</key>
|
<key>UTTypeDescription</key>
|
||||||
<string>ASCII Armored Signature</string>
|
<string>ASCII Armored File</string>
|
||||||
<key>UTTypeIconFile</key>
|
<key>UTTypeIconFile</key>
|
||||||
<string>sparrow.icns</string>
|
<string>sparrow.icns</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|
|
@ -21,6 +21,8 @@ import com.sparrowwallet.sparrow.control.*;
|
||||||
import com.sparrowwallet.sparrow.event.*;
|
import com.sparrowwallet.sparrow.event.*;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.*;
|
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.ElectrumServer;
|
||||||
import com.sparrowwallet.sparrow.net.ServerType;
|
import com.sparrowwallet.sparrow.net.ServerType;
|
||||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||||
|
@ -289,9 +291,7 @@ public class AppController implements Initializable {
|
||||||
Dragboard db = event.getDragboard();
|
Dragboard db = event.getDragboard();
|
||||||
boolean success = false;
|
boolean success = false;
|
||||||
if(db.hasFiles()) {
|
if(db.hasFiles()) {
|
||||||
for(File file : db.getFiles()) {
|
openFiles(db.getFiles());
|
||||||
openFile(file);
|
|
||||||
}
|
|
||||||
success = true;
|
success = true;
|
||||||
}
|
}
|
||||||
event.setDropCompleted(success);
|
event.setDropCompleted(success);
|
||||||
|
@ -333,6 +333,8 @@ public class AppController implements Initializable {
|
||||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyList()));
|
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyList()));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tabs.setPickOnBounds(false);
|
||||||
|
|
||||||
registerShortcuts();
|
registerShortcuts();
|
||||||
|
|
||||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||||
|
@ -379,6 +381,9 @@ public class AppController implements Initializable {
|
||||||
preventSleepProperty.set(Config.get().isPreventSleep());
|
preventSleepProperty.set(Config.get().isPreventSleep());
|
||||||
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
|
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));
|
List<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET));
|
||||||
networks.remove(Network.get());
|
networks.remove(Network.get());
|
||||||
for(Network network : networks) {
|
for(Network network : networks) {
|
||||||
|
@ -636,7 +641,7 @@ public class AppController implements Initializable {
|
||||||
} catch(TransactionParseException e) {
|
} catch(TransactionParseException e) {
|
||||||
showErrorDialog("Invalid transaction", e.getMessage());
|
showErrorDialog("Invalid transaction", e.getMessage());
|
||||||
} catch(Exception e) {
|
} 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();
|
Transaction transaction = transactionTabData.getTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
UR ur = UR.fromBytes(transaction.bitcoinSerialize());
|
byte[] txBytes = transaction.bitcoinSerialize();
|
||||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur);
|
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.initOwner(rootStack.getScene().getWindow());
|
||||||
qrDisplayDialog.showAndWait();
|
qrDisplayDialog.showAndWait();
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
|
@ -851,8 +858,10 @@ public class AppController implements Initializable {
|
||||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||||
|
|
||||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(transactionTabData.getPsbt().serialize());
|
byte[] psbtBytes = transactionTabData.getPsbt().serialize();
|
||||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR());
|
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.initOwner(rootStack.getScene().getWindow());
|
||||||
qrDisplayDialog.show();
|
qrDisplayDialog.show();
|
||||||
}
|
}
|
||||||
|
@ -967,19 +976,46 @@ public class AppController implements Initializable {
|
||||||
AppServices.get().setPreventSleep(item.isSelected());
|
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) {
|
public void restart(ActionEvent event, Network network) {
|
||||||
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
|
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
|
||||||
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
|
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();
|
Args args = new Args();
|
||||||
ProcessHandle.current().info().arguments().ifPresent(argv -> {
|
ProcessHandle.current().info().arguments().ifPresent(argv -> {
|
||||||
JCommander jCommander = JCommander.newBuilder().addObject(args).acceptUnknownOptions(true).build();
|
JCommander jCommander = JCommander.newBuilder().addObject(args).acceptUnknownOptions(true).build();
|
||||||
jCommander.parse(argv);
|
jCommander.parse(argv);
|
||||||
});
|
});
|
||||||
|
|
||||||
args.network = network;
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restart(ActionEvent event, Args args) {
|
||||||
try {
|
try {
|
||||||
List<String> cmd = new ArrayList<>();
|
List<String> cmd = new ArrayList<>();
|
||||||
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
||||||
|
@ -992,13 +1028,19 @@ public class AppController implements Initializable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void openFile(File file) {
|
public void openFiles(List<File> files) {
|
||||||
if(isWalletFile(file)) {
|
boolean verifyOpened = false;
|
||||||
openWalletFile(file, true);
|
for(File file : files) {
|
||||||
} else if(isVerifyDownloadFile(file)) {
|
if(isWalletFile(file)) {
|
||||||
verifyDownload(new ActionEvent(file, rootStack));
|
openWalletFile(file, true);
|
||||||
} else {
|
} else if(isVerifyDownloadFile(file)) {
|
||||||
openTransactionFile(file);
|
if(!verifyOpened) {
|
||||||
|
verifyDownload(new ActionEvent(file, rootStack));
|
||||||
|
verifyOpened = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
openTransactionFile(file);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2743,7 +2785,7 @@ public class AppController implements Initializable {
|
||||||
public void disconnection(DisconnectionEvent event) {
|
public void disconnection(DisconnectionEvent event) {
|
||||||
serverToggle.setDisable(false);
|
serverToggle.setDisable(false);
|
||||||
if(!AppServices.isConnecting() && !AppServices.isConnected() && !statusBar.getText().startsWith(CONNECTION_FAILED_PREFIX) && !statusBar.getText().contains(TRYING_ANOTHER_SERVER_MESSAGE)) {
|
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) {
|
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||||
statusBar.setProgress(0);
|
statusBar.setProgress(0);
|
||||||
|
|
|
@ -117,6 +117,8 @@ public class AppServices {
|
||||||
|
|
||||||
private ElectrumServer.ConnectionService connectionService;
|
private ElectrumServer.ConnectionService connectionService;
|
||||||
|
|
||||||
|
private ElectrumServer.FeeRatesService feeRatesService;
|
||||||
|
|
||||||
private Hwi.ScheduledEnumerateService deviceEnumerateService;
|
private Hwi.ScheduledEnumerateService deviceEnumerateService;
|
||||||
|
|
||||||
private VersionCheckService versionCheckService;
|
private VersionCheckService versionCheckService;
|
||||||
|
@ -188,6 +190,7 @@ public class AppServices {
|
||||||
public void start() {
|
public void start() {
|
||||||
Config config = Config.get();
|
Config config = Config.get();
|
||||||
connectionService = createConnectionService();
|
connectionService = createConnectionService();
|
||||||
|
feeRatesService = createFeeRatesService();
|
||||||
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
|
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
|
||||||
versionCheckService = createVersionCheckService();
|
versionCheckService = createVersionCheckService();
|
||||||
torService = createTorService();
|
torService = createTorService();
|
||||||
|
@ -201,6 +204,8 @@ public class AppServices {
|
||||||
} else {
|
} else {
|
||||||
restartServices();
|
restartServices();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
EventManager.get().post(new DisconnectionEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
addURIHandlers();
|
addURIHandlers();
|
||||||
|
@ -284,8 +289,15 @@ public class AppServices {
|
||||||
onlineProperty.setValue(true);
|
onlineProperty.setValue(true);
|
||||||
onlineProperty.addListener(onlineServicesListener);
|
onlineProperty.addListener(onlineServicesListener);
|
||||||
|
|
||||||
if(connectionService.getValue() != null) {
|
FeeRatesUpdatedEvent event = connectionService.getValue();
|
||||||
EventManager.get().post(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 -> {
|
connectionService.setOnFailed(failEvent -> {
|
||||||
|
@ -356,6 +368,15 @@ public class AppServices {
|
||||||
return connectionService;
|
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) {
|
private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) {
|
||||||
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(
|
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(
|
||||||
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
|
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
|
||||||
|
@ -1102,17 +1123,18 @@ public class AppServices {
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void mempoolRateSizes(MempoolRateSizesUpdatedEvent event) {
|
public void mempoolRateSizes(MempoolRateSizesUpdatedEvent event) {
|
||||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
if(event.getMempoolRateSizes() != null) {
|
||||||
|
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
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
|
//Perform once-off fee rates retrieval to immediately change displayed rates
|
||||||
feeRatesService.start();
|
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
|
||||||
|
feeRatesService = createFeeRatesService();
|
||||||
|
feeRatesService.start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
|
|
|
@ -16,9 +16,9 @@ import java.io.File;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
public class SparrowWallet {
|
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_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_VERSION_SUFFIX = "";
|
||||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||||
|
@ -79,7 +79,7 @@ public class SparrowWallet {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
instance = new Instance(fileUriArguments);
|
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) {
|
} catch(InstanceException e) {
|
||||||
getLogger().error("Could not access application lock", e);
|
getLogger().error("Could not access application lock", e);
|
||||||
}
|
}
|
||||||
|
@ -130,13 +130,13 @@ public class SparrowWallet {
|
||||||
private final List<String> fileUriArguments;
|
private final List<String> fileUriArguments;
|
||||||
|
|
||||||
public Instance(List<String> fileUriArguments) {
|
public Instance(List<String> fileUriArguments) {
|
||||||
super(SparrowWallet.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
|
super(SparrowWallet.APP_ID, true);
|
||||||
this.fileUriArguments = fileUriArguments;
|
this.fileUriArguments = fileUriArguments;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void receiveMessageList(List<String> messageList) {
|
protected void receiveMessageList(List<String> messageList) {
|
||||||
if(messageList != null && !messageList.isEmpty()) {
|
if(messageList != null) {
|
||||||
AppServices.parseFileUriArguments(messageList);
|
AppServices.parseFileUriArguments(messageList);
|
||||||
AppServices.openFileUriArguments(null);
|
AppServices.openFileUriArguments(null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package com.sparrowwallet.sparrow.control;
|
package com.sparrowwallet.sparrow.control;
|
||||||
|
|
||||||
import com.sparrowwallet.drongo.Utils;
|
import com.sparrowwallet.drongo.Utils;
|
||||||
|
import com.sparrowwallet.drongo.pgp.PGPKeySource;
|
||||||
import com.sparrowwallet.drongo.pgp.PGPUtils;
|
import com.sparrowwallet.drongo.pgp.PGPUtils;
|
||||||
import com.sparrowwallet.drongo.pgp.PGPVerificationException;
|
import com.sparrowwallet.drongo.pgp.PGPVerificationException;
|
||||||
import com.sparrowwallet.drongo.pgp.PGPVerificationResult;
|
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> publicKey = new SimpleObjectProperty<>();
|
||||||
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
|
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
|
private final BooleanProperty manifestDisabled = new SimpleBooleanProperty();
|
||||||
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
|
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
|
||||||
|
|
||||||
private final Label signedBy;
|
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";
|
String version = VersionCheckService.getVersion() != null ? VersionCheckService.getVersion() : "x.x.x";
|
||||||
|
|
||||||
Field signatureField = setupField(signature, "Signature", SIGNATURE_EXTENSIONS, false, "sparrow-" + version + "-manifest.txt", null);
|
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 publicKeyField = setupField(publicKey, "Public Key", PUBLIC_KEY_EXTENSIONS, true, "pgp_keys", publicKeyDisabled);
|
||||||
Field releaseFileField = setupField(release, "Release File", getReleaseFileExtensions(), false, getReleaseFileExample(version), null);
|
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);
|
release.set(null);
|
||||||
signedBy.setText("");
|
signedBy.setText("");
|
||||||
signedBy.setGraphic(null);
|
signedBy.setGraphic(null);
|
||||||
|
signedBy.setTooltip(null);
|
||||||
releaseHash.setText("");
|
releaseHash.setText("");
|
||||||
releaseHash.setGraphic(null);
|
releaseHash.setGraphic(null);
|
||||||
releaseVerified.setText("");
|
releaseVerified.setText("");
|
||||||
|
@ -262,6 +265,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void verify() {
|
private void verify() {
|
||||||
|
manifestDisabled.set(false);
|
||||||
publicKeyDisabled.set(false);
|
publicKeyDisabled.set(false);
|
||||||
|
|
||||||
if(signature.get() == null || manifest.get() == null) {
|
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)" : "");
|
String message = result.userId() + " on " + signatureDateFormat.format(result.signatureTimestamp()) + (result.expired() ? " (key expired)" : "");
|
||||||
signedBy.setText(message);
|
signedBy.setText(message);
|
||||||
signedBy.setGraphic(result.expired() ? GlyphUtils.getWarningGlyph() : GlyphUtils.getSuccessGlyph());
|
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);
|
publicKeyDisabled.set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(manifest.get().equals(release.get())) {
|
if(manifest.get().equals(release.get())) {
|
||||||
|
manifestDisabled.set(true);
|
||||||
releaseHash.setText("No hash required, signature signs release file directly");
|
releaseHash.setText("No hash required, signature signs release file directly");
|
||||||
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
|
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
|
||||||
releaseHash.setTooltip(null);
|
releaseHash.setTooltip(null);
|
||||||
|
@ -302,6 +308,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
Throwable e = event.getSource().getException();
|
Throwable e = event.getSource().getException();
|
||||||
signedBy.setText(getDisplayMessage(e));
|
signedBy.setText(getDisplayMessage(e));
|
||||||
signedBy.setGraphic(GlyphUtils.getFailureGlyph());
|
signedBy.setGraphic(GlyphUtils.getFailureGlyph());
|
||||||
|
signedBy.setTooltip(null);
|
||||||
clearReleaseFields();
|
clearReleaseFields();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -585,12 +585,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
getItems().add(createCpfp);
|
getItems().add(createCpfp);
|
||||||
}
|
}
|
||||||
|
|
||||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
if(!Config.get().isBlockExplorerDisabled()) {
|
||||||
openBlockExplorer.setOnAction(AE -> {
|
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||||
hide();
|
openBlockExplorer.setOnAction(AE -> {
|
||||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
hide();
|
||||||
});
|
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||||
getItems().add(openBlockExplorer);
|
});
|
||||||
|
getItems().add(openBlockExplorer);
|
||||||
|
}
|
||||||
|
|
||||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||||
copyTxid.setOnAction(AE -> {
|
copyTxid.setOnAction(AE -> {
|
||||||
|
@ -612,12 +614,16 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
hide();
|
hide();
|
||||||
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
||||||
});
|
});
|
||||||
|
getItems().add(viewTransaction);
|
||||||
|
|
||||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
if(!Config.get().isBlockExplorerDisabled()) {
|
||||||
openBlockExplorer.setOnAction(AE -> {
|
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||||
hide();
|
openBlockExplorer.setOnAction(AE -> {
|
||||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
hide();
|
||||||
});
|
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||||
|
});
|
||||||
|
getItems().add(openBlockExplorer);
|
||||||
|
}
|
||||||
|
|
||||||
MenuItem copyDate = new MenuItem("Copy Date");
|
MenuItem copyDate = new MenuItem("Copy Date");
|
||||||
copyDate.setOnAction(AE -> {
|
copyDate.setOnAction(AE -> {
|
||||||
|
@ -626,6 +632,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
content.putString(date);
|
content.putString(date);
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
getItems().add(copyDate);
|
||||||
|
|
||||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||||
copyTxid.setOnAction(AE -> {
|
copyTxid.setOnAction(AE -> {
|
||||||
|
@ -634,6 +641,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||||
content.putString(blockTransaction.getHashAsString());
|
content.putString(blockTransaction.getHashAsString());
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
getItems().add(copyTxid);
|
||||||
|
|
||||||
MenuItem copyHeight = new MenuItem("Copy Block Height");
|
MenuItem copyHeight = new MenuItem("Copy Block Height");
|
||||||
copyHeight.setOnAction(AE -> {
|
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");
|
content.putString(blockTransaction.getHeight() > 0 ? Integer.toString(blockTransaction.getHeight()) : "Mempool");
|
||||||
Clipboard.getSystemClipboard().setContent(content);
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
});
|
});
|
||||||
|
getItems().add(copyHeight);
|
||||||
getItems().addAll(viewTransaction, openBlockExplorer, copyDate, copyTxid, copyHeight);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,14 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
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.AppServices;
|
||||||
import com.sparrowwallet.sparrow.EventManager;
|
import com.sparrowwallet.sparrow.EventManager;
|
||||||
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
|
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.*;
|
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.geometry.Pos;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ButtonType;
|
import javafx.scene.control.ButtonType;
|
||||||
|
@ -153,7 +155,9 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
|
||||||
} else {
|
} else {
|
||||||
QRDisplayDialog qrDisplayDialog;
|
QRDisplayDialog qrDisplayDialog;
|
||||||
if(exporter instanceof Bip129) {
|
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 {
|
} else {
|
||||||
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
|
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.OutputDescriptor;
|
||||||
import com.sparrowwallet.drongo.SecureString;
|
import com.sparrowwallet.drongo.SecureString;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import com.sparrowwallet.hummingbird.UR;
|
||||||
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
|
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
|
||||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
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.event.WalletExportEvent;
|
||||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.*;
|
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.Service;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
@ -163,8 +166,12 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||||
QRDisplayDialog qrDisplayDialog;
|
QRDisplayDialog qrDisplayDialog;
|
||||||
if(exporter instanceof CoboVaultMultisig) {
|
if(exporter instanceof CoboVaultMultisig) {
|
||||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
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);
|
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) {
|
} else if(exporter instanceof Descriptor) {
|
||||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||||
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
|
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
|
||||||
|
|
|
@ -472,7 +472,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(qrText, true);
|
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(qrText, true);
|
||||||
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
|
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||||
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
|
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();
|
scanQr();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||||
import com.sparrowwallet.sparrow.io.Config;
|
import com.sparrowwallet.sparrow.io.Config;
|
||||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder;
|
import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder;
|
||||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRException;
|
import com.sparrowwallet.sparrow.io.bbqr.BBQRException;
|
||||||
|
import com.sparrowwallet.sparrow.wallet.KeystoreController;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.DoubleProperty;
|
import javafx.beans.property.DoubleProperty;
|
||||||
import javafx.beans.property.ObjectProperty;
|
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)
|
List<ChildNumber> path = cryptoKeypath.getComponents().stream().map(comp -> (IndexPathComponent)comp)
|
||||||
.map(comp -> new ChildNumber(comp.getIndex(), comp.isHardened())).collect(Collectors.toList());
|
.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;
|
return null;
|
||||||
|
|
|
@ -54,10 +54,10 @@ public class ScriptArea extends CodeArea {
|
||||||
ScriptChunk chunk = script.getChunks().get(i);
|
ScriptChunk chunk = script.getChunks().get(i);
|
||||||
if(chunk.isOpCode()) {
|
if(chunk.isOpCode()) {
|
||||||
append(chunk.toString(), "script-opcode");
|
append(chunk.toString(), "script-opcode");
|
||||||
} else if(chunk.isSignature()) {
|
|
||||||
append("<signature" + signatureCount++ + ">", "script-signature");
|
|
||||||
} else if(chunk.isPubKey()) {
|
} else if(chunk.isPubKey()) {
|
||||||
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
||||||
|
} else if(chunk.isSignature()) {
|
||||||
|
append("<signature" + signatureCount++ + ">", "script-signature");
|
||||||
} else if(chunk.isTaprootControlBlock()) {
|
} else if(chunk.isTaprootControlBlock()) {
|
||||||
append("<controlblock>", "script-controlblock");
|
append("<controlblock>", "script-controlblock");
|
||||||
} else if(chunk.isString()) {
|
} else if(chunk.isString()) {
|
||||||
|
|
|
@ -1,535 +1,270 @@
|
||||||
/**
|
|
||||||
* 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;
|
package com.sparrowwallet.sparrow.instance;
|
||||||
|
|
||||||
|
import com.sparrowwallet.sparrow.io.Storage;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.File;
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.net.ConnectException;
|
||||||
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.SocketException;
|
import java.net.SocketException;
|
||||||
import java.net.UnknownHostException;
|
import java.net.StandardProtocolFamily;
|
||||||
import java.nio.channels.FileChannel;
|
import java.net.UnixDomainSocketAddress;
|
||||||
import java.nio.channels.FileLock;
|
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 {
|
public abstract class Instance {
|
||||||
private static final Logger log = LoggerFactory.getLogger(Instance.class);
|
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
|
public final String applicationId;
|
||||||
private static final int PORT_START = 7221;
|
private final boolean autoExit;
|
||||||
|
|
||||||
// system temporary directory path
|
private Selector selector;
|
||||||
private static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
|
private ServerSocketChannel serverChannel;
|
||||||
|
|
||||||
/**
|
public Instance(final String applicationId) {
|
||||||
* Unique string representing the application ID.<br><br>
|
this(applicationId, true);
|
||||||
*
|
}
|
||||||
* 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
|
public Instance(final String applicationId, final boolean autoExit) {
|
||||||
private final boolean AUTO_EXIT;
|
this.applicationId = applicationId;
|
||||||
|
this.autoExit = autoExit;
|
||||||
|
}
|
||||||
|
|
||||||
// lock server port
|
/**
|
||||||
private int port;
|
* Try to obtain lock. If not possible, send data to first instance.
|
||||||
|
*
|
||||||
|
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
|
||||||
|
*/
|
||||||
|
public void acquireLock(boolean findExisting) throws InstanceException {
|
||||||
|
Path lockFile = getLockFile(findExisting);
|
||||||
|
|
||||||
// lock server socket
|
if(!Files.exists(lockFile)) {
|
||||||
private ServerSocket server;
|
startServer(lockFile);
|
||||||
|
} else {
|
||||||
|
doClient(lockFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// lock file RAF object
|
private void startServer(Path lockFile) throws InstanceException {
|
||||||
private RandomAccessFile lockRAF;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// file lock for the lock file RAF object
|
Thread thread = new Thread(() -> {
|
||||||
private FileLock fileLock;
|
while(true) {
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if(key.isReadable()) {
|
||||||
|
try(SocketChannel clientChannel = (SocketChannel)key.channel()) {
|
||||||
|
String message = readMessage(clientChannel);
|
||||||
|
clientChannel.write(ByteBuffer.wrap(applicationId.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
receiveMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iter.remove();
|
||||||
|
}
|
||||||
|
} catch(SocketException e) {
|
||||||
|
if(serverChannel.isOpen()) {
|
||||||
|
handleException(new InstanceException(e));
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
handleException(new InstanceException(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
thread.setDaemon(true);
|
||||||
* Parameterized constructor.<br>
|
thread.setName("SparrowInstanceListener");
|
||||||
* This constructor configures to automatically exit the application for subsequent instances.<br><br>
|
thread.start();
|
||||||
*
|
|
||||||
* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
createSymlink(lockFile);
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
private void doClient(Path lockFile) throws InstanceException {
|
||||||
* Try to obtain lock. If not possible, send data to first instance.
|
try(SocketChannel client = SocketChannel.open(UnixDomainSocketAddress.of(lockFile))) {
|
||||||
*
|
String message = sendMessage();
|
||||||
* @deprecated Use <code>acquireLock()</code> instead.
|
client.write(ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8)));
|
||||||
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
|
client.shutdownOutput();
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public void lock() throws InstanceException {
|
|
||||||
acquireLock();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
String response = readMessage(client);
|
||||||
* Try to obtain lock. If not possible, send data to first instance.
|
if(response.equals(applicationId) && autoExit) {
|
||||||
*
|
beforeExit();
|
||||||
* @since 1.2
|
System.exit(0);
|
||||||
*
|
}
|
||||||
* @return true if able to acquire lock, false otherwise
|
} catch(ConnectException e) {
|
||||||
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
|
try {
|
||||||
*/
|
Files.deleteIfExists(lockFile);
|
||||||
public boolean acquireLock() throws InstanceException {
|
startServer(lockFile);
|
||||||
// try to obtain port number from lock file
|
} catch(Exception ex) {
|
||||||
port = lockFile();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (port == -1) {
|
private static String readMessage(SocketChannel clientChannel) throws IOException {
|
||||||
// failed to fetch port number
|
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||||
// try to start server
|
StringBuilder messageBuilder = new StringBuilder();
|
||||||
startServer();
|
while(clientChannel.read(buffer) != -1) {
|
||||||
}
|
buffer.flip();
|
||||||
else {
|
messageBuilder.append(new String(buffer.array(), 0, buffer.limit()));
|
||||||
// port number fetched from lock file
|
buffer.clear();
|
||||||
// try to start client
|
}
|
||||||
doClient();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (server != null);
|
return messageBuilder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// start the server
|
private Path getLockFile(boolean findExisting) {
|
||||||
private void startServer() throws InstanceException {
|
if(findExisting) {
|
||||||
// try to create server
|
Path pointer = getUserLockFilePointer();
|
||||||
port = PORT_START;
|
try {
|
||||||
while (true) {
|
if(pointer != null && Files.exists(pointer)) {
|
||||||
try {
|
if(Files.isSymbolicLink(pointer)) {
|
||||||
server = new ServerSocket(port, 50, InetAddress.getByName(null));
|
return Files.readSymbolicLink(pointer);
|
||||||
break;
|
} else {
|
||||||
} catch (IOException e) {
|
Path lockFile = Path.of(Files.readString(pointer, StandardCharsets.UTF_8));
|
||||||
port++;
|
if(Files.exists(lockFile)) {
|
||||||
}
|
return lockFile;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Could not find lock file at " + pointer.toAbsolutePath());
|
||||||
|
} catch(Exception e) {
|
||||||
|
//ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// try to lock file
|
return Storage.getSparrowDir().toPath().resolve(applicationId + ".lock");
|
||||||
lockFile(port);
|
}
|
||||||
|
|
||||||
// server created successfully; this is the first instance
|
private void createSymlink(Path lockFile) {
|
||||||
// keep listening for data from other instances
|
Path pointer = getUserLockFilePointer();
|
||||||
Thread thread = new Thread() {
|
try {
|
||||||
@Override
|
if(pointer != null && !Files.exists(pointer, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
public void run() {
|
Files.createSymbolicLink(pointer, lockFile);
|
||||||
while (!server.isClosed()) {
|
pointer.toFile().deleteOnExit();
|
||||||
try {
|
}
|
||||||
// establish connection
|
} catch(IOException e) {
|
||||||
final Socket socket = server.accept();
|
log.debug("Could not create symlink " + pointer.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath() + ", writing as normal file", e);
|
||||||
|
|
||||||
// handle socket on a different thread to allow parallel connections
|
try {
|
||||||
Thread thread = new Thread() {
|
Files.writeString(pointer, lockFile.toAbsolutePath().toString(), StandardCharsets.UTF_8);
|
||||||
@Override
|
pointer.toFile().deleteOnExit();
|
||||||
public void run() {
|
} catch(IOException ex) {
|
||||||
try {
|
log.warn("Could not create pointer " + pointer.toAbsolutePath() + " to lockFile at " + lockFile.toAbsolutePath(), ex);
|
||||||
// open writer
|
}
|
||||||
OutputStream os = socket.getOutputStream();
|
} catch(Exception e) {
|
||||||
DataOutputStream dos = new DataOutputStream(os);
|
//ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// open reader
|
private Path getUserLockFilePointer() {
|
||||||
InputStream is = socket.getInputStream();
|
if(Boolean.parseBoolean(System.getenv(LINK_ENV_PROPERTY))) {
|
||||||
DataInputStream dis = new DataInputStream(is);
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// read message length from client
|
try {
|
||||||
int length = dis.readInt();
|
File sparrowHome = Storage.getSparrowHome(true);
|
||||||
|
if(!sparrowHome.exists()) {
|
||||||
|
Storage.createOwnerOnlyDirectory(sparrowHome);
|
||||||
|
}
|
||||||
|
|
||||||
// read message string from client
|
return sparrowHome.toPath().resolve(applicationId + ".default");
|
||||||
String message = null;
|
} catch(Exception e) {
|
||||||
if (length > -1) {
|
return null;
|
||||||
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) {
|
* Free the lock if possible. This is only required to be called from the first instance.
|
||||||
dos.writeInt(-1);
|
*
|
||||||
}
|
* @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock
|
||||||
else {
|
*/
|
||||||
byte[] appId = APP_ID.getBytes("UTF-8");
|
public void freeLock() throws InstanceException {
|
||||||
|
try {
|
||||||
|
if(serverChannel != null && serverChannel.isOpen()) {
|
||||||
|
serverChannel.close();
|
||||||
|
}
|
||||||
|
if(getUserLockFilePointer() != null) {
|
||||||
|
Files.deleteIfExists(getUserLockFilePointer());
|
||||||
|
}
|
||||||
|
Files.deleteIfExists(getLockFile(false));
|
||||||
|
} catch(Exception e) {
|
||||||
|
throw new InstanceException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dos.writeInt(appId.length);
|
/**
|
||||||
dos.write(appId);
|
* Method used in first instance to receive messages from subsequent instances.<br><br>
|
||||||
}
|
*
|
||||||
dos.flush();
|
* This method is not synchronized.
|
||||||
|
*
|
||||||
|
* @param message message received by first instance from subsequent instances
|
||||||
|
*/
|
||||||
|
protected abstract void receiveMessage(String message);
|
||||||
|
|
||||||
// close writer and reader
|
/**
|
||||||
dos.close();
|
* Method used in subsequent instances to send message to first instance.<br><br>
|
||||||
dis.close();
|
*
|
||||||
|
* It is not recommended to perform blocking (long running) tasks here. Use <code>beforeExit()</code> method instead.<br>
|
||||||
|
* One exception to this rule is if you intend to perform some user interaction before sending the message.<br><br>
|
||||||
|
*
|
||||||
|
* This method is not synchronized.
|
||||||
|
*
|
||||||
|
* @return message sent from subsequent instances
|
||||||
|
*/
|
||||||
|
protected abstract String sendMessage();
|
||||||
|
|
||||||
// perform user action on message
|
/**
|
||||||
receiveMessage(message);
|
* Method to receive and handle exceptions occurring while first instance is listening for subsequent instances.<br><br>
|
||||||
|
*
|
||||||
// close socket
|
* By default prints stack trace of all exceptions. Override this method to handle exceptions explicitly.<br><br>
|
||||||
socket.close();
|
*
|
||||||
} catch (IOException e) {
|
* This method is not synchronized.
|
||||||
handleException(new InstanceException(e));
|
*
|
||||||
}
|
* @param exception exception occurring while first instance is listening for subsequent instances
|
||||||
}
|
*/
|
||||||
};
|
protected void handleException(Exception exception) {
|
||||||
|
|
||||||
// start socket thread
|
|
||||||
thread.start();
|
|
||||||
} catch (SocketException e) {
|
|
||||||
if (!server.isClosed()) {
|
|
||||||
handleException(new InstanceException(e));
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
handleException(new InstanceException(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
thread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
String message = sendMessage();
|
|
||||||
|
|
||||||
// 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
|
|
||||||
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
|
|
||||||
try {
|
|
||||||
if (socket != null) socket.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InstanceException(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
}
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
throw new InstanceException(e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InstanceException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to close lock file RAF object
|
|
||||||
if (lockRAF != null) {
|
|
||||||
lockRAF.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to delete lock file
|
|
||||||
if (file.exists()) {
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InstanceException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method used in first instance to receive messages from subsequent instances.<br><br>
|
|
||||||
*
|
|
||||||
* This method is not synchronized.
|
|
||||||
*
|
|
||||||
* @param message message received by first instance from subsequent instances
|
|
||||||
*/
|
|
||||||
protected abstract void receiveMessage(String message);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method used in subsequent instances to send message to first instance.<br><br>
|
|
||||||
*
|
|
||||||
* It is not recommended to perform blocking (long running) tasks here. Use <code>beforeExit()</code> method instead.<br>
|
|
||||||
* One exception to this rule is if you intend to perform some user interaction before sending the message.<br><br>
|
|
||||||
*
|
|
||||||
* This method is not synchronized.
|
|
||||||
*
|
|
||||||
* @return message sent from subsequent instances
|
|
||||||
*/
|
|
||||||
protected abstract String sendMessage();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to receive and handle exceptions occurring while first instance is listening for subsequent instances.<br><br>
|
|
||||||
*
|
|
||||||
* By default prints stack trace of all exceptions. Override this method to handle exceptions explicitly.<br><br>
|
|
||||||
*
|
|
||||||
* This method is not synchronized.
|
|
||||||
*
|
|
||||||
* @param exception exception occurring while first instance is listening for subsequent instances
|
|
||||||
*/
|
|
||||||
protected void handleException(Exception exception) {
|
|
||||||
log.error("Error listening for instances", exception);
|
log.error("Error listening for instances", exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is called before exiting from subsequent instances.<br><br>
|
|
||||||
*
|
|
||||||
* Override this method to perform blocking tasks before exiting from subsequent instances.<br>
|
|
||||||
* This method is not invoked if auto exit is turned off.<br><br>
|
|
||||||
*
|
|
||||||
* This method is not synchronized.
|
|
||||||
*
|
|
||||||
* @since 1.2
|
|
||||||
*/
|
|
||||||
protected void beforeExit() {}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method is called before exiting from subsequent instances.<br><br>
|
||||||
|
*
|
||||||
|
* Override this method to perform blocking tasks before exiting from subsequent instances.<br>
|
||||||
|
* This method is not invoked if auto exit is turned off.<br><br>
|
||||||
|
*
|
||||||
|
* This method is not synchronized.
|
||||||
|
*/
|
||||||
|
protected void beforeExit() {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,88 +7,14 @@ import com.google.gson.JsonArray;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonParser;
|
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 {
|
public abstract class InstanceList extends Instance {
|
||||||
|
|
||||||
/**
|
public InstanceList(String applicationId) {
|
||||||
* Parameterized constructor.<br>
|
super(applicationId);
|
||||||
* 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, boolean autoExit) {
|
||||||
* Parameterized constructor.<br>
|
super(applicationId, autoExit);
|
||||||
* 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,16 +27,15 @@ public abstract class InstanceList extends Instance {
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected final void receiveMessage(String message) {
|
protected final void receiveMessage(String message) {
|
||||||
if (message == null) {
|
if(message == null) {
|
||||||
receiveMessageList(null);
|
receiveMessageList(null);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
// parse the JSON array string into an array of string arguments
|
// parse the JSON array string into an array of string arguments
|
||||||
JsonArray jsonArgs = JsonParser.parseString(message).getAsJsonArray();
|
JsonArray jsonArgs = JsonParser.parseString(message).getAsJsonArray();
|
||||||
|
|
||||||
List<String> stringArgs = new ArrayList<String>(jsonArgs.size());
|
List<String> stringArgs = new ArrayList<String>(jsonArgs.size());
|
||||||
|
|
||||||
for (int i = 0; i < jsonArgs.size(); i++) {
|
for(int i = 0; i < jsonArgs.size(); i++) {
|
||||||
JsonElement element = jsonArgs.get(i);
|
JsonElement element = jsonArgs.get(i);
|
||||||
stringArgs.add(element.getAsString());
|
stringArgs.add(element.getAsString());
|
||||||
}
|
}
|
||||||
|
@ -137,10 +62,11 @@ public abstract class InstanceList extends Instance {
|
||||||
JsonArray jsonArgs = new JsonArray();
|
JsonArray jsonArgs = new JsonArray();
|
||||||
|
|
||||||
List<String> stringArgs = sendMessageList();
|
List<String> stringArgs = sendMessageList();
|
||||||
|
if(stringArgs == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (stringArgs == null) return null;
|
for(String arg : stringArgs) {
|
||||||
|
|
||||||
for (String arg : stringArgs) {
|
|
||||||
jsonArgs.add(arg);
|
jsonArgs.add(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,5 +94,4 @@ public abstract class InstanceList extends Instance {
|
||||||
* @return list of messages sent from subsequent instances
|
* @return list of messages sent from subsequent instances
|
||||||
*/
|
*/
|
||||||
protected abstract List<String> sendMessageList();
|
protected abstract List<String> sendMessageList();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -535,7 +535,7 @@ public class Storage {
|
||||||
return certsDir;
|
return certsDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
static File getSparrowDir() {
|
public static File getSparrowDir() {
|
||||||
File sparrowDir;
|
File sparrowDir;
|
||||||
if(Network.get() != Network.MAINNET) {
|
if(Network.get() != Network.MAINNET) {
|
||||||
sparrowDir = new File(getSparrowHome(), Network.get().getName());
|
sparrowDir = new File(getSparrowHome(), Network.get().getName());
|
||||||
|
@ -551,7 +551,11 @@ public class Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static File getSparrowHome() {
|
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));
|
return new File(System.getProperty(SparrowWallet.APP_HOME_PROPERTY));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -812,12 +812,18 @@ public class ElectrumServer {
|
||||||
return transactionMap;
|
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);
|
Map<Integer, Double> targetBlocksFeeRatesSats = getDefaultFeeEstimates(targetBlocks);
|
||||||
|
|
||||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
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));
|
targetBlocksFeeRatesSats.putAll(feeRatesSource.getBlockTargetFeeRates(targetBlocksFeeRatesSats));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1204,7 +1210,7 @@ public class ElectrumServer {
|
||||||
|
|
||||||
String banner = electrumServer.getServerBanner();
|
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();
|
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||||
|
|
||||||
|
@ -1220,7 +1226,7 @@ public class ElectrumServer {
|
||||||
|
|
||||||
long elapsed = System.currentTimeMillis() - feeRatesRetrievedAt;
|
long elapsed = System.currentTimeMillis() - feeRatesRetrievedAt;
|
||||||
if(elapsed > FEE_RATES_PERIOD) {
|
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();
|
Set<MempoolRateSize> mempoolRateSizes = electrumServer.getMempoolRateSizes();
|
||||||
feeRatesRetrievedAt = System.currentTimeMillis();
|
feeRatesRetrievedAt = System.currentTimeMillis();
|
||||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
||||||
|
@ -1679,9 +1685,8 @@ public class ElectrumServer {
|
||||||
return new Task<>() {
|
return new Task<>() {
|
||||||
protected FeeRatesUpdatedEvent call() throws ServerException {
|
protected FeeRatesUpdatedEvent call() throws ServerException {
|
||||||
ElectrumServer electrumServer = new ElectrumServer();
|
ElectrumServer electrumServer = new ElectrumServer();
|
||||||
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();
|
return new FeeRatesUpdatedEvent(blockTargetFeeRates, null);
|
||||||
return new FeeRatesUpdatedEvent(blockTargetFeeRates, mempoolRateSizes);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,27 +9,27 @@ import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public enum FeeRatesSource {
|
public enum FeeRatesSource {
|
||||||
ELECTRUM_SERVER("Server") {
|
ELECTRUM_SERVER("Server", false) {
|
||||||
@Override
|
@Override
|
||||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||||
return Collections.emptyMap();
|
return Collections.emptyMap();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MEMPOOL_SPACE("mempool.space") {
|
MEMPOOL_SPACE("mempool.space", true) {
|
||||||
@Override
|
@Override
|
||||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
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";
|
String url = AppServices.isUsingProxy() ? "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1/fees/recommended" : "https://mempool.space/api/v1/fees/recommended";
|
||||||
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
BITCOINFEES_EARN_COM("bitcoinfees.earn.com") {
|
BITCOINFEES_EARN_COM("bitcoinfees.earn.com", true) {
|
||||||
@Override
|
@Override
|
||||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||||
String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended";
|
String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended";
|
||||||
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
return getThreeTierFeeRates(this, defaultblockTargetFeeRates, url);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
MINIMUM("Minimum (1 sat/vB)") {
|
MINIMUM("Minimum (1 sat/vB)", false) {
|
||||||
@Override
|
@Override
|
||||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||||
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
|
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
|
||||||
|
@ -40,7 +40,7 @@ public enum FeeRatesSource {
|
||||||
return blockTargetFeeRates;
|
return blockTargetFeeRates;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
OXT_ME("oxt.me") {
|
OXT_ME("oxt.me", true) {
|
||||||
@Override
|
@Override
|
||||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
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";
|
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;
|
public static final int BLOCKS_IN_TWO_HOURS = 12;
|
||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
|
private final boolean external;
|
||||||
|
|
||||||
FeeRatesSource(String name) {
|
FeeRatesSource(String name, boolean external) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.external = external;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
|
public abstract Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates);
|
||||||
|
@ -74,6 +76,10 @@ public enum FeeRatesSource {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isExternal() {
|
||||||
|
return external;
|
||||||
|
}
|
||||||
|
|
||||||
private static Map<Integer, Double> getThreeTierFeeRates(FeeRatesSource feeRatesSource, Map<Integer, Double> defaultblockTargetFeeRates, String url) {
|
private static Map<Integer, Double> getThreeTierFeeRates(FeeRatesSource feeRatesSource, Map<Integer, Double> defaultblockTargetFeeRates, String url) {
|
||||||
if(log.isInfoEnabled()) {
|
if(log.isInfoEnabled()) {
|
||||||
log.info("Requesting fee rates from " + url);
|
log.info("Requesting fee rates from " + url);
|
||||||
|
|
|
@ -1,16 +1,23 @@
|
||||||
package com.sparrowwallet.sparrow.net;
|
package com.sparrowwallet.sparrow.net;
|
||||||
|
|
||||||
import com.google.common.net.HostAndPort;
|
import com.google.common.net.HostAndPort;
|
||||||
|
import com.sparrowwallet.drongo.Utils;
|
||||||
import com.sparrowwallet.sparrow.AppServices;
|
import com.sparrowwallet.sparrow.AppServices;
|
||||||
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal;
|
import io.matthewnelson.kmp.tor.controller.common.control.usecase.TorControlSignal;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.*;
|
||||||
import java.net.Socket;
|
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 {
|
public class TorUtils {
|
||||||
private static final Logger log = LoggerFactory.getLogger(TorUtils.class);
|
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) {
|
public static void changeIdentity(HostAndPort proxy) {
|
||||||
if(AppServices.isTorRunning()) {
|
if(AppServices.isTorRunning()) {
|
||||||
|
@ -22,16 +29,62 @@ public class TorUtils {
|
||||||
} else {
|
} else {
|
||||||
HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1);
|
HostAndPort control = HostAndPort.fromParts(proxy.getHost(), proxy.getPort() + 1);
|
||||||
try(Socket socket = new Socket(control.getHost(), control.getPort())) {
|
try(Socket socket = new Socket(control.getHost(), control.getPort())) {
|
||||||
writeNewNym(socket);
|
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) {
|
} catch(Exception e) {
|
||||||
log.warn("Error connecting to " + control + ", no Tor ControlPort configured?");
|
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 {
|
private static void writeNewNym(Socket socket) throws IOException {
|
||||||
log.debug("Sending NEWNYM to " + socket);
|
log.debug("Sending NEWNYM to " + socket);
|
||||||
socket.getOutputStream().write("AUTHENTICATE \"\"\r\n".getBytes());
|
|
||||||
socket.getOutputStream().write("SIGNAL NEWNYM\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
|
//TODO: Remove once Cobo Vault support has been removed
|
||||||
boolean addLegacyEncodingOption = headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().equals(WalletModel.COBO_VAULT));
|
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));
|
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
|
//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;
|
-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 {
|
.drag-over > .background-text {
|
||||||
-fx-fill: #383a42;
|
-fx-fill: #383a42;
|
||||||
}
|
}
|
||||||
|
|
|
@ -143,7 +143,7 @@
|
||||||
<MenuItem mnemonicParsing="false" text="Verify Download" onAction="#verifyDownload" />
|
<MenuItem mnemonicParsing="false" text="Verify Download" onAction="#verifyDownload" />
|
||||||
<MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/>
|
<MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/>
|
||||||
<CheckMenuItem fx:id="preventSleep" mnemonicParsing="false" text="Prevent Computer Sleep" onAction="#preventSleep"/>
|
<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>
|
||||||
<Menu fx:id="helpMenu" mnemonicParsing="false" text="Help">
|
<Menu fx:id="helpMenu" mnemonicParsing="false" text="Help">
|
||||||
<MenuItem mnemonicParsing="false" text="Show Introduction" onAction="#showIntroduction"/>
|
<MenuItem mnemonicParsing="false" text="Show Introduction" onAction="#showIntroduction"/>
|
||||||
|
@ -156,12 +156,13 @@
|
||||||
</menus>
|
</menus>
|
||||||
</MenuBar>
|
</MenuBar>
|
||||||
<DecorationPane fx:id="rootStack" VBox.vgrow="ALWAYS">
|
<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">
|
<HBox alignment="CENTER">
|
||||||
<VBox alignment="CENTER_LEFT" spacing="15">
|
<VBox alignment="CENTER" spacing="15">
|
||||||
<Text styleClass="background-text" text="File menu → New Wallet or" />
|
<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>
|
||||||
<Text styleClass="background-text" text="File menu → Import Wallet or" />
|
<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>
|
||||||
<Text styleClass="background-text" text="Drag files here to open" />
|
<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>
|
</VBox>
|
||||||
</HBox>
|
</HBox>
|
||||||
<TabPane fx:id="tabs" />
|
<TabPane fx:id="tabs" />
|
||||||
|
|
|
@ -156,11 +156,25 @@ HorizontalHeaderColumn > TableColumnHeader.column-header.table-column{
|
||||||
-fx-border-color: #626367;
|
-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%);
|
-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;
|
-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.Keystore;
|
||||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ import java.io.*;
|
||||||
public class StorageTest extends IoTest {
|
public class StorageTest extends IoTest {
|
||||||
@Test
|
@Test
|
||||||
public void loadWallet() throws IOException, MnemonicException, StorageException {
|
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"));
|
Storage storage = new Storage(getFile("sparrow-single-wallet"));
|
||||||
Wallet wallet = storage.loadEncryptedWallet("pass").getWallet();
|
Wallet wallet = storage.loadEncryptedWallet("pass").getWallet();
|
||||||
Assertions.assertTrue(wallet.isValid());
|
Assertions.assertTrue(wallet.isValid());
|
||||||
|
@ -64,6 +66,7 @@ public class StorageTest extends IoTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void saveWallet() throws IOException, MnemonicException, StorageException {
|
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"));
|
Storage storage = new Storage(getFile("sparrow-single-wallet"));
|
||||||
Wallet wallet = storage.loadEncryptedWallet("pass").getWallet();
|
Wallet wallet = storage.loadEncryptedWallet("pass").getWallet();
|
||||||
Assertions.assertTrue(wallet.isValid());
|
Assertions.assertTrue(wallet.isValid());
|
||||||
|
@ -80,4 +83,9 @@ public class StorageTest extends IoTest {
|
||||||
wallet = temp2Storage.loadEncryptedWallet("pass").getWallet();
|
wallet = temp2Storage.loadEncryptedWallet("pass").getWallet();
|
||||||
Assertions.assertTrue(wallet.isValid());
|
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
|
Name: CC-2-of-4
|
||||||
Policy: 2 of 4
|
Policy: 2 of 4
|
||||||
Derivation: m/48'/1'/0'/2'
|
Derivation: m/48'/0'/0'/2'
|
||||||
Format: P2WSH
|
Format: P2WSH
|
||||||
|
|
||||||
0F056943: xpub6EfEGa5isJbQFSswM5Uptw5BSq2Td1ZDJr3QUNUcMySpC7itZ3ccypVHtLPnvMzKQ2qxrAgH49vhVxRcaQLFbixAVRR8RACrYTp88Uv9h8Z
|
0F056943: xpub6EfEGa5isJbQFSswM5Uptw5BSq2Td1ZDJr3QUNUcMySpC7itZ3ccypVHtLPnvMzKQ2qxrAgH49vhVxRcaQLFbixAVRR8RACrYTp88Uv9h8Z
|
||||||
|
|
Loading…
Reference in a new issue