mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7f6f7f3db | ||
|
|
dd9868c918 | ||
|
|
c2d3afae59 | ||
|
|
04a516d56b | ||
|
|
b27709e96f | ||
|
|
ebb7d23a05 | ||
|
|
97d121244f | ||
|
|
60dbc8ed84 | ||
|
|
29cd321724 | ||
|
|
467b834955 | ||
|
|
3bfa5d3dc4 | ||
|
|
f501f08e17 | ||
|
|
7d796369d6 | ||
|
|
c572011578 | ||
|
|
dbc1e7746b | ||
|
|
9325a1968b | ||
|
|
85166635b4 | ||
|
|
ed69a86529 | ||
|
|
ab2c77695b | ||
|
|
6ad81e1228 | ||
|
|
ff340c2449 | ||
|
|
f2b0f8ca9e | ||
|
|
0c213294ad | ||
|
|
7cdb7319ee | ||
|
|
d1ff8d6e3e | ||
|
|
d9ddc74d73 | ||
|
|
603df6d0f6 | ||
|
|
cbf847a57f | ||
|
|
e8fb676a24 | ||
|
|
273f3043fb | ||
|
|
8f165b05c7 | ||
|
|
8eb092a8d6 | ||
|
|
8dd1850905 | ||
|
|
0fa6bd56e2 | ||
|
|
c4c581525a | ||
|
|
d6a3824690 | ||
|
|
7dba141073 | ||
|
|
f2f6e639dc | ||
|
|
78afc5e4d5 | ||
|
|
b2d85b6c78 | ||
|
|
f3b0d37c54 | ||
|
|
6768ad2028 | ||
|
|
81cde4756a | ||
|
|
778564a954 | ||
|
|
1e4c8c3837 | ||
|
|
22408103ea | ||
|
|
383d3954d8 | ||
|
|
77a4e4aa50 | ||
|
|
19dedfa070 | ||
|
|
52696b014f | ||
|
|
8fb6de85f1 | ||
|
|
8270eb71db | ||
|
|
edcf12de5a | ||
|
|
da3399468c | ||
|
|
923c61fceb | ||
|
|
d3d939889e | ||
|
|
2b4d3fac6c | ||
|
|
1f67692727 | ||
|
|
bacbdb848b | ||
|
|
51ba7fc4cf | ||
|
|
59efed9e42 | ||
|
|
193fea4b69 | ||
|
|
b5cc2ff4c3 | ||
|
|
252ec58065 | ||
|
|
b8979ed8b0 | ||
|
|
c24f953e52 | ||
|
|
d139ca2706 | ||
|
|
bd421e877a | ||
|
|
25e1250710 | ||
|
|
fad1dad76e | ||
|
|
b3bd42b8f6 | ||
|
|
5aea538f09 | ||
|
|
16755e3140 | ||
|
|
cbfb7230a8 | ||
|
|
e438389953 | ||
|
|
6534ccb07e | ||
|
|
ca782dfc69 | ||
|
|
f1a662ba8a | ||
|
|
33fb2a38fc | ||
|
|
8b22e057bf |
@@ -19,12 +19,6 @@ jobs:
|
||||
java-version: '18.0.1'
|
||||
- name: Show Build Versions
|
||||
run: ./gradlew -v
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew jpackage
|
||||
- name: Package zip distribution
|
||||
@@ -34,9 +28,26 @@ jobs:
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew packageTarDistribution
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }}
|
||||
path: |
|
||||
build/jpackage/*
|
||||
!build/jpackage/Sparrow/
|
||||
- name: Headless build with Gradle
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew -Djava.awt.headless=true clean jpackage
|
||||
- name: Package headless tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
||||
- name: Rename Headless Artifacts
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done;
|
||||
- name: Upload Headless Artifact
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }} Headless
|
||||
path: |
|
||||
build/jpackage/*
|
||||
!build/jpackage/Sparrow/
|
||||
@@ -16,8 +16,8 @@ or for those without SSH credentials:
|
||||
|
||||
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
|
||||
|
||||
In order to build, Sparrow requires Java 17 to be installed.
|
||||
The release binaries are built with [Eclipse Temurin 17.0.2+8](https://github.com/adoptium/temurin17-binaries/releases/tag/jdk-17.0.2%2B8).
|
||||
In order to build, Sparrow requires Java 17 or higher to be installed.
|
||||
The release binaries are built with [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
|
||||
@@ -36,7 +36,7 @@ When updating to the latest HEAD
|
||||
|
||||
The release binaries are reproducible from v1.5.0 onwards (pre codesigning and installer packaging). More detailed [instructions on reproducing the binaries](docs/reproducible.md) are provided.
|
||||
|
||||
> A video documention of your built process uploaded to [bitcoinbinary.org](https://bitcoinbinary.org/) is appreciated. Alternatively check the site if you wish to see if someone else already verified the provided binaries.
|
||||
> Video documentation of your build process uploaded to [bitcoinbinary.org](https://bitcoinbinary.org/) is appreciated. Alternatively check the site if you wish to see if someone else already verified the provided binaries.
|
||||
|
||||
## Running
|
||||
|
||||
@@ -77,11 +77,11 @@ Note that if you are connecting to an Electrum server when using testnet, that s
|
||||
|
||||
When not explicitly configured using the command line argument above, Sparrow stores its mainnet config file, log file and wallets in a home folder location appropriate to the operating system:
|
||||
|
||||
Platform | Location
|
||||
-------- | --------
|
||||
OSX | ~/.sparrow
|
||||
Linux | ~/.sparrow
|
||||
Windows | %APPDATA%/Sparrow
|
||||
| Platform | Location |
|
||||
|----------| -------- |
|
||||
| OSX | ~/.sparrow |
|
||||
| Linux | ~/.sparrow |
|
||||
| Windows | %APPDATA%/Sparrow |
|
||||
|
||||
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
|
||||
|
||||
+17
-5
@@ -1,11 +1,13 @@
|
||||
import java.awt.GraphicsEnvironment
|
||||
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.13'
|
||||
id 'org-openjfx-javafxplugin'
|
||||
id 'extra-java-module-info'
|
||||
id 'org.beryx.jlink' version '2.25.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.6.6'
|
||||
def sparrowVersion = '1.7.0'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -13,10 +15,13 @@ if(os.macOsX) {
|
||||
}
|
||||
def targetName = ""
|
||||
def osArch = "x64"
|
||||
def releaseArch = "x86_64"
|
||||
if(System.getProperty("os.arch") == "aarch64") {
|
||||
osArch = "aarch64"
|
||||
releaseArch = "aarch64"
|
||||
targetName = "-" + osArch
|
||||
}
|
||||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||
|
||||
group "com.sparrowwallet"
|
||||
version "${sparrowVersion}"
|
||||
@@ -104,12 +109,13 @@ dependencies {
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.27')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.1')
|
||||
testImplementation('junit:junit:4.12')
|
||||
}
|
||||
|
||||
application {
|
||||
mainModule = 'com.sparrowwallet.sparrow'
|
||||
mainClass = 'com.sparrowwallet.sparrow.MainApp'
|
||||
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
||||
}
|
||||
|
||||
compileJava {
|
||||
@@ -158,6 +164,9 @@ run {
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
||||
}
|
||||
if(headless) {
|
||||
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||
}
|
||||
}
|
||||
|
||||
jlink {
|
||||
@@ -177,7 +186,7 @@ jlink {
|
||||
uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory'
|
||||
}
|
||||
|
||||
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
launcher {
|
||||
name = 'sparrow'
|
||||
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
@@ -210,6 +219,9 @@ jlink {
|
||||
if(os.macOsX) {
|
||||
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
|
||||
}
|
||||
if(headless) {
|
||||
jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||
}
|
||||
}
|
||||
addExtraDependencies("javafx")
|
||||
jpackage {
|
||||
@@ -251,7 +263,7 @@ task packageZipDistribution(type: Zip) {
|
||||
|
||||
task packageTarDistribution(type: Tar) {
|
||||
dependsOn removeGroupWritePermission
|
||||
archiveFileName = "sparrow-${sparrowVersion}.tar.gz"
|
||||
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
compression = Compression.GZIP
|
||||
from("$buildDir/jpackage/") {
|
||||
|
||||
@@ -4,10 +4,15 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
implementation 'org.ow2.asm:asm:8.0.1'
|
||||
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.0'
|
||||
implementation 'org.javamodularity:moduleplugin:1.8.12'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
}
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
@@ -17,5 +22,9 @@ gradlePlugin {
|
||||
id = "extra-java-module-info"
|
||||
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
|
||||
}
|
||||
register("org-openjfx-javafxplugin") {
|
||||
id = "org-openjfx-javafxplugin"
|
||||
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 )
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
+57
-22
@@ -1,56 +1,88 @@
|
||||
## Reproducible builds
|
||||
# Reproducible builds
|
||||
|
||||
Reproducibility is a goal of the Sparrow Wallet project.
|
||||
Reproducibility is a goal of the Sparrow Wallet project.
|
||||
As of v1.5.0 and later, it is possible to recreate the exact binaries in the Github releases (specifically, the contents of the `.tar.gz` and `.zip` files).
|
||||
|
||||
Due to minor variances, it is not yet possible to reproduce the installer packages (`.deb`, `.rpm` and `.exe`).
|
||||
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
|
||||
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
|
||||
Work on resolving both of these issues is ongoing.
|
||||
|
||||
### Reproducing a release
|
||||
## Reproducing a release
|
||||
|
||||
#### Install Java
|
||||
### Install Java
|
||||
|
||||
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
|
||||
For v1.6.4 and later, this is Eclipse Temurin 17.0.2+8.
|
||||
It is available for all supported platforms from [Eclipse Temurin 17.0.2+8](https://github.com/adoptium/temurin17-binaries/releases/tag/jdk-17.0.2%2B8).
|
||||
For v1.6.6 and later, this is Eclipse Temurin 18.0.1+10.
|
||||
|
||||
#### Java from Adoptium github repo
|
||||
|
||||
It is available for all supported platforms from [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
|
||||
|
||||
For reference, the downloads are as follows:
|
||||
- [Linux x64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.2_8.tar.gz)
|
||||
- [MacOS x64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_mac_hotspot_17.0.2_8.tar.gz)
|
||||
- [MacOS aarch64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_aarch64_mac_hotspot_17.0.2_8.tar.gz)
|
||||
- [Windows x64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_windows_hotspot_17.0.2_8.zip)
|
||||
- [Linux x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz)
|
||||
- [MacOS x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_mac_hotspot_18.0.1_10.tar.gz)
|
||||
- [MacOS aarch64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_aarch64_mac_hotspot_18.0.1_10.tar.gz)
|
||||
- [Windows x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_windows_hotspot_18.0.1_10.zip)
|
||||
|
||||
#### Java from Adoptium deb repo
|
||||
|
||||
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
|
||||
```shell
|
||||
sudo apt-get install -y wget apt-transport-https gnupg
|
||||
wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add -
|
||||
echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
|
||||
|
||||
- Install dependencies:
|
||||
```sh
|
||||
sudo apt-get install -y wget curl apt-transport-https gnupg
|
||||
```
|
||||
|
||||
Download Adoptium public PGP key:
|
||||
```sh
|
||||
curl --tlsv1.2 --proto =https --location -o adoptium.asc https://packages.adoptium.net/artifactory/api/gpg/key/public
|
||||
```
|
||||
|
||||
Check if key fingerprint matches: `3B04D753C9050D9A5D343F39843C48A565F8F04B`:
|
||||
```
|
||||
gpg --import --import-options show-only adoptium.asc
|
||||
```
|
||||
If key doesn't match, do not procede.
|
||||
|
||||
Add Adoptium PGP key to a the keyring shared folder:
|
||||
```sh
|
||||
sudo cp adoptium.asc /usr/share/keyrings/
|
||||
```
|
||||
|
||||
Add Adoptium debian repository:
|
||||
```sh
|
||||
echo "deb [signed-by=/usr/share/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
|
||||
```
|
||||
|
||||
Update cache, install the desired temurin version and configure java to be linked to this same version:
|
||||
```
|
||||
sudo apt update -y
|
||||
sudo apt-get install -y temurin-17-jdk=17.0.2+8
|
||||
sudo apt-get install -y temurin-18-jdk=18.0.1+10
|
||||
sudo update-alternatives --config java
|
||||
```
|
||||
|
||||
#### Java from SDK
|
||||
|
||||
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
|
||||
See the installation [instructions here](https://sdkman.io/install).
|
||||
Once installed, run
|
||||
```shell
|
||||
sdk install java 17.0.2-tem
|
||||
sdk install java 18.0.1-tem
|
||||
```
|
||||
|
||||
#### Other requirements
|
||||
### Other requirements
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
```shell
|
||||
sudo apt install -y rpm fakeroot binutils
|
||||
```
|
||||
|
||||
#### Building the binaries
|
||||
### Building the binaries
|
||||
|
||||
The project can cloned for a specific release tag as follows:
|
||||
```shell
|
||||
GIT_TAG="1.6.6"
|
||||
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
|
||||
GIT_TAG="1.7.0"
|
||||
git clone --recursive --branch "${GIT_TAG}" https://github.com/sparrowwallet/sparrow.git
|
||||
```
|
||||
|
||||
Thereafter, building should be straightforward:
|
||||
@@ -62,7 +94,9 @@ cd sparrow
|
||||
|
||||
The binaries (and installers) will be placed in the `build/jpackage` folder.
|
||||
|
||||
#### Verifying the binaries are identical
|
||||
### Verifying the binaries are identical
|
||||
|
||||
Verify the built binaries against the released binaries on https://github.com/sparrowwallet/sparrow/releases.
|
||||
|
||||
Note that you will be verifying the files in the `build/jpackage/Sparrow` folder against either the `.tar.gz` or `.zip` releases.
|
||||
Download either of these depending on your platform and extract the contents to a folder (in the following example, `/tmp`).
|
||||
@@ -74,3 +108,4 @@ diff -r build/jpackage/Sparrow /tmp/Sparrow
|
||||
|
||||
This command should have no output indicating that the two folders (and all their contents) are identical.
|
||||
|
||||
If there is output, please open an issue with detailed instructions to reproduce, including build system platform.
|
||||
|
||||
+1
-1
Submodule drongo updated: 8cdea77562...7c34ec7c3b
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.6.6</string>
|
||||
<string>1.7.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -12,7 +12,7 @@ public class AboutController {
|
||||
private Label title;
|
||||
|
||||
public void initializeView() {
|
||||
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION + MainApp.APP_VERSION_SUFFIX);
|
||||
title.setText(SparrowWallet.APP_NAME + " " + SparrowWallet.APP_VERSION + SparrowWallet.APP_VERSION_SUFFIX);
|
||||
}
|
||||
|
||||
public void setStage(Stage stage) {
|
||||
|
||||
@@ -35,7 +35,6 @@ import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import de.codecentric.centerdevice.MenuToolkit;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
@@ -132,6 +131,9 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private ToggleGroup bitcoinUnit;
|
||||
|
||||
@FXML
|
||||
private ToggleGroup unitFormat;
|
||||
|
||||
@FXML
|
||||
private ToggleGroup theme;
|
||||
|
||||
@@ -182,6 +184,9 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem showPayNym;
|
||||
|
||||
@FXML
|
||||
private Menu switchServer;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem preventSleep;
|
||||
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
|
||||
@@ -322,6 +327,15 @@ public class AppController implements Initializable {
|
||||
Optional<Toggle> selectedUnitToggle = bitcoinUnit.getToggles().stream().filter(toggle -> selectedUnit.equals(toggle.getUserData())).findFirst();
|
||||
selectedUnitToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle));
|
||||
|
||||
UnitFormat format = Config.get().getUnitFormat();
|
||||
if(format == null) {
|
||||
format = UnitFormat.DOT;
|
||||
Config.get().setUnitFormat(format);
|
||||
}
|
||||
final UnitFormat selectedFormat = format;
|
||||
Optional<Toggle> selectedFormatToggle = unitFormat.getToggles().stream().filter(toggle -> selectedFormat.equals(toggle.getUserData())).findFirst();
|
||||
selectedFormatToggle.ifPresent(toggle -> unitFormat.selectToggle(toggle));
|
||||
|
||||
Theme configTheme = Config.get().getTheme();
|
||||
if(configTheme == null) {
|
||||
configTheme = Theme.LIGHT;
|
||||
@@ -367,6 +381,7 @@ public class AppController implements Initializable {
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||
});
|
||||
|
||||
configureSwitchServer();
|
||||
setServerType(Config.get().getServerType());
|
||||
serverToggle.setSelected(isConnected());
|
||||
serverToggle.setDisable(Config.get().getServerType() == null);
|
||||
@@ -377,6 +392,7 @@ public class AppController implements Initializable {
|
||||
});
|
||||
|
||||
openTransactionIdItem.disableProperty().bind(onlineProperty().not());
|
||||
setNetworkLabel();
|
||||
}
|
||||
|
||||
private void setPlatformApplicationMenu() {
|
||||
@@ -386,10 +402,10 @@ public class AppController implements Initializable {
|
||||
MenuItem preferences = new MenuItem("Preferences...");
|
||||
preferences.setOnAction(this::openPreferences);
|
||||
preferences.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN));
|
||||
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(MainApp.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
|
||||
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(SparrowWallet.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
|
||||
preferences, new SeparatorMenuItem(),
|
||||
tk.createHideMenuItem(MainApp.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
|
||||
tk.createQuitMenuItem(MainApp.APP_NAME));
|
||||
tk.createHideMenuItem(SparrowWallet.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
|
||||
tk.createQuitMenuItem(SparrowWallet.APP_NAME));
|
||||
tk.setApplicationMenu(defaultApplicationMenu);
|
||||
|
||||
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||
@@ -404,12 +420,27 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
private void setNetworkLabel() {
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
Platform.runLater(() -> {
|
||||
StackPane tabBackground = (StackPane)tabs.lookup(".tab-header-background");
|
||||
if(tabBackground != null) {
|
||||
HBox hBox = new HBox();
|
||||
Label label = new Label(Network.get().toDisplayString());
|
||||
label.setPadding(new Insets(0, 10, 0, 0));
|
||||
hBox.getChildren().add(label);
|
||||
hBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
tabBackground.getChildren().add(hBox);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void showIntroduction(ActionEvent event) {
|
||||
WelcomeDialog welcomeDialog = new WelcomeDialog();
|
||||
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
|
||||
if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER);
|
||||
preferencesDialog.showAndWait();
|
||||
openPreferences(PreferenceGroup.SERVER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,7 +481,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
Stage stage = new Stage(StageStyle.UNDECORATED);
|
||||
stage.setTitle("About " + MainApp.APP_NAME);
|
||||
stage.setTitle("About " + SparrowWallet.APP_NAME);
|
||||
stage.initOwner(tabs.getScene().getWindow());
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.setResizable(false);
|
||||
@@ -855,6 +886,13 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new BitcoinUnitChangedEvent(unit));
|
||||
}
|
||||
|
||||
public void setUnitFormat(ActionEvent event) {
|
||||
MenuItem item = (MenuItem)event.getSource();
|
||||
UnitFormat format = (UnitFormat)item.getUserData();
|
||||
Config.get().setUnitFormat(format);
|
||||
EventManager.get().post(new UnitFormatChangedEvent(format));
|
||||
}
|
||||
|
||||
public void preventSleep(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setPreventSleep(item.isSelected());
|
||||
@@ -902,7 +940,7 @@ public class AppController implements Initializable {
|
||||
|
||||
private String getServerToggleTooltipText(Integer currentBlockHeight) {
|
||||
if(AppServices.isConnected()) {
|
||||
return "Connected to " + Config.get().getServerAddress() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") +
|
||||
return "Connected to " + Config.get().getServerDisplayName() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") +
|
||||
(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? "\nWarning! You are connected to a public server and sharing your transaction data with it.\nFor better privacy, consider using your own Bitcoin Core node or private Electrum server." : "");
|
||||
}
|
||||
|
||||
@@ -996,7 +1034,7 @@ public class AppController implements Initializable {
|
||||
} catch(Exception e) {
|
||||
if(e instanceof IOException && e.getMessage().startsWith("The process cannot access the file because another process has locked")) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", "The wallet file is locked. Is another instance of " + MainApp.APP_NAME + " already running?");
|
||||
showErrorDialog("Error Opening Wallet", "The wallet file is locked. Is another instance of " + SparrowWallet.APP_NAME + " already running?");
|
||||
} else if(!attemptImportWallet(file, null)) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage() == null ? "Unsupported file format" : e.getMessage());
|
||||
@@ -1006,8 +1044,7 @@ public class AppController implements Initializable {
|
||||
|
||||
private void openWallet(Storage storage, WalletAndKey walletAndKey, AppController appController, boolean forceSameWindow) {
|
||||
try {
|
||||
checkWalletNetwork(walletAndKey.getWallet());
|
||||
restorePublicKeysFromSeed(storage, walletAndKey.getWallet(), walletAndKey.getKey());
|
||||
storage.restorePublicKeysFromSeed(walletAndKey.getWallet(), walletAndKey.getKey());
|
||||
if(!walletAndKey.getWallet().isValid()) {
|
||||
throw new IllegalStateException("Wallet file is not valid.");
|
||||
}
|
||||
@@ -1026,97 +1063,6 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
private void checkWalletNetwork(Wallet wallet) {
|
||||
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.get()) {
|
||||
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.get() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
|
||||
}
|
||||
}
|
||||
|
||||
private void restorePublicKeysFromSeed(Storage storage, Wallet wallet, Key key) throws MnemonicException {
|
||||
if(wallet.containsMasterPrivateKeys()) {
|
||||
//Derive xpub and master fingerprint from seed, potentially with passphrase
|
||||
Wallet copy = wallet.copy();
|
||||
for(int i = 0; i < copy.getKeystores().size(); i++) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
if(copyKeystore.hasSeed() && copyKeystore.getSeed().getPassphrase() == null) {
|
||||
if(copyKeystore.getSeed().needsPassphrase()) {
|
||||
if(!wallet.isMasterWallet() && wallet.getMasterWallet().getKeystores().size() == copy.getKeystores().size() && wallet.getMasterWallet().getKeystores().get(i).hasSeed()) {
|
||||
copyKeystore.getSeed().setPassphrase(wallet.getMasterWallet().getKeystores().get(i).getSeed().getPassphrase());
|
||||
} else {
|
||||
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getFullDisplayName(), copyKeystore);
|
||||
Optional<String> optionalPassphrase = passphraseDialog.showAndWait();
|
||||
if(optionalPassphrase.isPresent()) {
|
||||
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
copyKeystore.getSeed().setPassphrase("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(wallet.isEncrypted()) {
|
||||
if(key == null) {
|
||||
throw new IllegalStateException("Wallet was not encrypted, but seed is");
|
||||
}
|
||||
|
||||
copy.decrypt(key);
|
||||
}
|
||||
|
||||
if(wallet.isWhirlpoolMasterWallet()) {
|
||||
String walletId = storage.getWalletId(wallet);
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
|
||||
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
soroban.setHDWallet(copy);
|
||||
}
|
||||
|
||||
StandardAccount standardAccount = wallet.getStandardAccountType();
|
||||
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
|
||||
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
|
||||
}
|
||||
|
||||
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
||||
Keystore keystore = wallet.getKeystores().get(i);
|
||||
if(keystore.hasSeed()) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
Keystore derivedKeystore = Keystore.fromSeed(copyKeystore.getSeed(), copyKeystore.getKeyDerivation().getDerivation());
|
||||
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
||||
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
||||
keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase());
|
||||
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
||||
copyKeystore.getSeed().clear();
|
||||
} else if(keystore.hasMasterPrivateExtendedKey()) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), copyKeystore.getKeyDerivation().getDerivation());
|
||||
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
||||
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
||||
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
||||
copyKeystore.getMasterPrivateKey().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isBip47()) {
|
||||
try {
|
||||
Keystore masterKeystore = wallet.getKeystores().get(0);
|
||||
Keystore keystore = childWallet.getKeystores().get(0);
|
||||
keystore.setBip47ExtendedPrivateKey(masterKeystore.getBip47ExtendedPrivateKey());
|
||||
List<ChildNumber> derivation = keystore.getKeyDerivation().getDerivation();
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterKeystore.getKeyDerivation().getMasterFingerprint(), derivation));
|
||||
DeterministicKey pubKey = keystore.getBip47ExtendedPrivateKey().getKey().dropPrivateBytes().dropParent();
|
||||
keystore.setExtendedPublicKey(new ExtendedKey(pubKey, keystore.getBip47ExtendedPrivateKey().getParentFingerprint(), derivation.get(derivation.size() - 1)));
|
||||
} catch(Exception e) {
|
||||
log.error("Cannot prepare BIP47 keystore", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void importWallet(ActionEvent event) {
|
||||
WalletImportDialog dlg = new WalletImportDialog();
|
||||
Optional<Wallet> optionalWallet = dlg.showAndWait();
|
||||
@@ -1136,6 +1082,10 @@ public class AppController implements Initializable {
|
||||
new KeystoneSinglesig(), new KeystoneMultisig(),
|
||||
new CaravanMultisig());
|
||||
for(WalletImport importer : walletImporters) {
|
||||
if(importer.isDeprecated() && !Config.get().isShowDeprecatedImportExport()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try(FileInputStream inputStream = new FileInputStream(file)) {
|
||||
if(importer.isEncrypted(file) && password == null) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
@@ -1212,14 +1162,12 @@ public class AppController implements Initializable {
|
||||
try {
|
||||
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||
storage.saveWallet(wallet);
|
||||
checkWalletNetwork(wallet);
|
||||
restorePublicKeysFromSeed(storage, wallet, null);
|
||||
storage.restorePublicKeysFromSeed(wallet, null);
|
||||
addWalletTabOrWindow(storage, wallet, false);
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
storage.saveWallet(childWallet);
|
||||
checkWalletNetwork(childWallet);
|
||||
restorePublicKeysFromSeed(storage, childWallet, null);
|
||||
storage.restorePublicKeysFromSeed(childWallet, null);
|
||||
addWalletTabOrWindow(storage, childWallet, false);
|
||||
}
|
||||
Platform.runLater(() -> selectTab(wallet));
|
||||
@@ -1239,8 +1187,7 @@ public class AppController implements Initializable {
|
||||
wallet.encrypt(key);
|
||||
storage.setEncryptionPubKey(encryptionPubKey);
|
||||
storage.saveWallet(wallet);
|
||||
checkWalletNetwork(wallet);
|
||||
restorePublicKeysFromSeed(storage, wallet, key);
|
||||
storage.restorePublicKeysFromSeed(wallet, key);
|
||||
addWalletTabOrWindow(storage, wallet, false);
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
@@ -1248,8 +1195,7 @@ public class AppController implements Initializable {
|
||||
childWallet.encrypt(key);
|
||||
}
|
||||
storage.saveWallet(childWallet);
|
||||
checkWalletNetwork(childWallet);
|
||||
restorePublicKeysFromSeed(storage, childWallet, key);
|
||||
storage.restorePublicKeysFromSeed(childWallet, key);
|
||||
addWalletTabOrWindow(storage, childWallet, false);
|
||||
}
|
||||
Platform.runLater(() -> selectTab(wallet));
|
||||
@@ -1284,13 +1230,17 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
public void openPreferences(ActionEvent event) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog();
|
||||
preferencesDialog.showAndWait();
|
||||
openPreferences(PreferenceGroup.GENERAL);
|
||||
}
|
||||
|
||||
public void openServerPreferences(ActionEvent event) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER);
|
||||
openPreferences(PreferenceGroup.SERVER);
|
||||
}
|
||||
|
||||
private void openPreferences(PreferenceGroup preferenceGroup) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(preferenceGroup);
|
||||
preferencesDialog.showAndWait();
|
||||
configureSwitchServer();
|
||||
}
|
||||
|
||||
public void signVerifyMessage(ActionEvent event) {
|
||||
@@ -1860,6 +1810,27 @@ public class AppController implements Initializable {
|
||||
contextMenu.getItems().addAll(lock);
|
||||
}
|
||||
|
||||
MenuItem moveRight = new MenuItem("Move Right");
|
||||
moveRight.setOnAction(event -> {
|
||||
int index = tabs.getTabs().indexOf(tab);
|
||||
tabs.getTabs().remove(tab);
|
||||
tabs.getTabs().add(index + 1, tab);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
});
|
||||
MenuItem moveLeft = new MenuItem("Move Left");
|
||||
moveLeft.setOnAction(event -> {
|
||||
int index = tabs.getTabs().indexOf(tab);
|
||||
tabs.getTabs().remove(tab);
|
||||
tabs.getTabs().add(index - 1, tab);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
});
|
||||
contextMenu.getItems().addAll(moveRight, moveLeft);
|
||||
|
||||
contextMenu.setOnShowing(event -> {
|
||||
moveRight.setDisable(tabs.getTabs().indexOf(tab) == tabs.getTabs().size() - 1);
|
||||
moveLeft.setDisable(tabs.getTabs().indexOf(tab) == 0);
|
||||
});
|
||||
|
||||
MenuItem close = new MenuItem("Close");
|
||||
close.setOnAction(event -> {
|
||||
tabs.getTabs().remove(tab);
|
||||
@@ -1877,12 +1848,16 @@ public class AppController implements Initializable {
|
||||
tabs.getTabs().removeAll(tabs.getTabs());
|
||||
});
|
||||
|
||||
MenuItem delete = new MenuItem("Delete...");
|
||||
delete.setOnAction(event -> {
|
||||
deleteWallet(getSelectedWalletForm());
|
||||
});
|
||||
contextMenu.getItems().addAll(new SeparatorMenuItem(), close, closeOthers, closeAll);
|
||||
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
MenuItem delete = new MenuItem("Delete...");
|
||||
delete.setOnAction(event -> {
|
||||
deleteWallet(getSelectedWalletForm());
|
||||
});
|
||||
contextMenu.getItems().addAll(new SeparatorMenuItem(), delete);
|
||||
}
|
||||
|
||||
contextMenu.getItems().addAll(close, closeOthers, closeAll, delete);
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
@@ -1971,6 +1946,48 @@ public class AppController implements Initializable {
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
private void configureSwitchServer() {
|
||||
switchServer.getItems().clear();
|
||||
|
||||
Config config = Config.get();
|
||||
if(config.getServerType() == ServerType.BITCOIN_CORE && config.getRecentCoreServers() != null && config.getRecentCoreServers().size() > 1) {
|
||||
for(Server server : config.getRecentCoreServers()) {
|
||||
switchServer.getItems().add(getSwitchServerMenuItem(ServerType.BITCOIN_CORE, server));
|
||||
}
|
||||
} else if(config.getServerType() == ServerType.ELECTRUM_SERVER && config.getRecentElectrumServers() != null && config.getRecentElectrumServers().size() > 1) {
|
||||
for(Server server : config.getRecentElectrumServers()) {
|
||||
switchServer.getItems().add(getSwitchServerMenuItem(ServerType.ELECTRUM_SERVER, server));
|
||||
}
|
||||
}
|
||||
|
||||
switchServer.setVisible(!switchServer.getItems().isEmpty());
|
||||
}
|
||||
|
||||
private CheckMenuItem getSwitchServerMenuItem(ServerType serverType, Server server) {
|
||||
CheckMenuItem checkMenuItem = new CheckMenuItem(server.getDisplayName());
|
||||
boolean selected = (serverType == ServerType.BITCOIN_CORE ? server.equals(Config.get().getCoreServer()) : server.equals(Config.get().getElectrumServer()));
|
||||
checkMenuItem.setSelected(selected);
|
||||
checkMenuItem.setOnAction(event -> {
|
||||
if(!selected) {
|
||||
boolean online = onlineProperty().get();
|
||||
onlineProperty().set(false);
|
||||
if(serverType == ServerType.BITCOIN_CORE) {
|
||||
Config.get().setCoreServer(server);
|
||||
} else if(serverType == ServerType.ELECTRUM_SERVER) {
|
||||
Config.get().setElectrumServer(server);
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
onlineProperty().set(online);
|
||||
configureSwitchServer();
|
||||
});
|
||||
} else {
|
||||
checkMenuItem.setSelected(true);
|
||||
}
|
||||
});
|
||||
|
||||
return checkMenuItem;
|
||||
}
|
||||
|
||||
public void setServerType(ServerType serverType) {
|
||||
if(serverType == ServerType.PUBLIC_ELECTRUM_SERVER && !serverToggle.getStyleClass().contains("public-server")) {
|
||||
serverToggle.getStyleClass().add("public-server");
|
||||
@@ -2399,11 +2416,12 @@ public class AppController implements Initializable {
|
||||
|
||||
@Subscribe
|
||||
public void connection(ConnectionEvent event) {
|
||||
String status = "Connected to " + Config.get().getServerAddress() + " at height " + event.getBlockHeight();
|
||||
String status = "Connected to " + Config.get().getServerDisplayName() + " at height " + event.getBlockHeight();
|
||||
statusUpdated(new StatusEvent(status));
|
||||
setServerToggleTooltip(event.getBlockHeight());
|
||||
serverToggleStopAnimation();
|
||||
setTorIcon();
|
||||
configureSwitchServer();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -2623,6 +2641,12 @@ public class AppController implements Initializable {
|
||||
selectedToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void unitFormatChanged(UnitFormatChangedEvent event) {
|
||||
Optional<Toggle> selectedToggle = unitFormat.getToggles().stream().filter(toggle -> event.getUnitFormat().equals(toggle.getUserData())).findFirst();
|
||||
selectedToggle.ifPresent(toggle -> unitFormat.selectToggle(toggle));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void openWalletsInNewWindowsStatusChanged(OpenWalletsNewWindowsStatusEvent event) {
|
||||
openWalletsInNewWindows.setSelected(event.isOpenWalletsInNewWindows());
|
||||
|
||||
@@ -20,7 +20,6 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
@@ -28,6 +27,7 @@ import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymService;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -42,7 +42,6 @@ import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
@@ -52,7 +51,6 @@ import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
import org.berndpruenster.netlayer.tor.Tor;
|
||||
import org.controlsfx.control.HyperlinkLabel;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -69,8 +67,6 @@ import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AppServices {
|
||||
@@ -97,9 +93,11 @@ public class AppServices {
|
||||
|
||||
private final SorobanServices sorobanServices = new SorobanServices();
|
||||
|
||||
private InteractionServices interactionServices;
|
||||
|
||||
private static PayNymService payNymService;
|
||||
|
||||
private final MainApp application;
|
||||
private final Application application;
|
||||
|
||||
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
|
||||
|
||||
@@ -173,8 +171,9 @@ public class AppServices {
|
||||
openFiles(event.getFiles(), null);
|
||||
};
|
||||
|
||||
public AppServices(MainApp application) {
|
||||
private AppServices(Application application, InteractionServices interactionServices) {
|
||||
this.application = application;
|
||||
this.interactionServices = interactionServices;
|
||||
EventManager.get().register(this);
|
||||
EventManager.get().register(whirlpoolServices);
|
||||
EventManager.get().register(sorobanServices);
|
||||
@@ -203,7 +202,7 @@ public class AppServices {
|
||||
|
||||
private void restartServices() {
|
||||
Config config = Config.get();
|
||||
if(config.hasServerAddress()) {
|
||||
if(config.hasServer()) {
|
||||
restartService(connectionService);
|
||||
}
|
||||
|
||||
@@ -211,7 +210,7 @@ public class AppServices {
|
||||
restartService(ratesService);
|
||||
}
|
||||
|
||||
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET) {
|
||||
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET && Interface.get() == Interface.DESKTOP) {
|
||||
restartService(versionCheckService);
|
||||
}
|
||||
|
||||
@@ -268,7 +267,7 @@ public class AppServices {
|
||||
connectionService.setOnRunning(workerStateEvent -> {
|
||||
connectionService.setDelay(Duration.ZERO);
|
||||
if(!ElectrumServer.isConnected()) {
|
||||
EventManager.get().post(new ConnectionStartEvent(Config.get().getServerAddress()));
|
||||
EventManager.get().post(new ConnectionStartEvent(Config.get().getServerDisplayName()));
|
||||
}
|
||||
});
|
||||
connectionService.setOnSucceeded(successEvent -> {
|
||||
@@ -499,8 +498,12 @@ public class AppServices {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
static void initialize(MainApp application) {
|
||||
INSTANCE = new AppServices(application);
|
||||
public static void initialize(Application application) {
|
||||
INSTANCE = new AppServices(application, new DefaultInteractionServices());
|
||||
}
|
||||
|
||||
public static void initialize(Application application, InteractionServices interactionServices) {
|
||||
INSTANCE = new AppServices(application, interactionServices);
|
||||
}
|
||||
|
||||
public static AppServices get() {
|
||||
@@ -515,6 +518,10 @@ public class AppServices {
|
||||
return get().sorobanServices;
|
||||
}
|
||||
|
||||
public static InteractionServices getInteractionServices() {
|
||||
return get().interactionServices;
|
||||
}
|
||||
|
||||
public static PayNymService getPayNymService() {
|
||||
if(payNymService == null) {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
@@ -561,7 +568,7 @@ public class AppServices {
|
||||
|
||||
private static Image getWindowIcon() {
|
||||
if(windowIcon == null) {
|
||||
windowIcon = new Image(MainApp.class.getResourceAsStream("/image/sparrow-icon.png"));
|
||||
windowIcon = new Image(SparrowWallet.class.getResourceAsStream("/image/sparrow-icon.png"));
|
||||
}
|
||||
|
||||
return windowIcon;
|
||||
@@ -571,7 +578,7 @@ public class AppServices {
|
||||
return (node.getScene() != null && node.getScene().getWindow().getHeight() < 768);
|
||||
}
|
||||
|
||||
public MainApp getApplication() {
|
||||
public Application getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
@@ -746,40 +753,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
|
||||
Alert alert = new Alert(alertType, content, buttons);
|
||||
setStageIcon(alert.getDialogPane().getScene().getWindow());
|
||||
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(title);
|
||||
if(graphic != null) {
|
||||
alert.setGraphic(graphic);
|
||||
}
|
||||
|
||||
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
|
||||
Matcher matcher = linkPattern.matcher(content);
|
||||
if(matcher.find()) {
|
||||
String link = matcher.group(1);
|
||||
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(content);
|
||||
hyperlinkLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
hyperlinkLabel.setMaxHeight(Double.MAX_VALUE);
|
||||
hyperlinkLabel.getStyleClass().add("content");
|
||||
Label label = new Label();
|
||||
hyperlinkLabel.setPrefWidth(Math.max(360, TextUtils.computeTextWidth(label.getFont(), link, 0.0D) + 50));
|
||||
hyperlinkLabel.setOnAction(event -> {
|
||||
alert.close();
|
||||
get().getApplication().getHostServices().showDocument(link);
|
||||
});
|
||||
alert.getDialogPane().setContent(hyperlinkLabel);
|
||||
}
|
||||
|
||||
String[] lines = content.split("\r\n|\r|\n");
|
||||
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
|
||||
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
|
||||
}
|
||||
|
||||
moveToActiveWindowScreen(alert);
|
||||
return alert.showAndWait();
|
||||
return getInteractionServices().showAlert(title, content, alertType, graphic, buttons);
|
||||
}
|
||||
|
||||
public static void setStageIcon(Window window) {
|
||||
@@ -1130,7 +1104,7 @@ public class AppServices {
|
||||
Wallet wallet = walletTabData.getWallet();
|
||||
Storage storage = walletTabData.getStorage();
|
||||
|
||||
if(!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
if(Interface.get() == Interface.DESKTOP && (!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB))) {
|
||||
usbWallet = true;
|
||||
|
||||
if(deviceEnumerateService == null) {
|
||||
@@ -1157,12 +1131,18 @@ public class AppServices {
|
||||
|
||||
@Subscribe
|
||||
public void requestConnect(RequestConnectEvent event) {
|
||||
onlineProperty.set(true);
|
||||
if(Config.get().hasServer()) {
|
||||
onlineProperty.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void requestDisconnect(RequestDisconnectEvent event) {
|
||||
onlineProperty.set(false);
|
||||
//Ensure services don't try to reconnect
|
||||
connectionService.cancel();
|
||||
ratesService.cancel();
|
||||
versionCheckService.cancel();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1209,7 +1189,7 @@ public class AppServices {
|
||||
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
|
||||
onlineProperty.set(false);
|
||||
log.warn("Failed to fetch wallet history from " + Config.get().getServerAddress() + ", reconnecting to another server...");
|
||||
log.warn("Failed to fetch wallet history from " + Config.get().getServerDisplayName() + ", reconnecting to another server...");
|
||||
Config.get().changePublicServer();
|
||||
onlineProperty.set(true);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ public class Args {
|
||||
@Parameter(names = { "--level", "-l" }, description = "Set log level")
|
||||
public Level level;
|
||||
|
||||
@Parameter(names = { "--terminal", "-t" }, description = "Terminal mode", arity = 0)
|
||||
public boolean terminal;
|
||||
|
||||
@Parameter(names = { "--version", "-v" }, description = "Show version", arity = 0)
|
||||
public boolean version;
|
||||
|
||||
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
|
||||
public boolean help;
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.text.Font;
|
||||
import org.controlsfx.control.HyperlinkLabel;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.moveToActiveWindowScreen;
|
||||
import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
|
||||
|
||||
public class DefaultInteractionServices implements InteractionServices {
|
||||
@Override
|
||||
public Optional<ButtonType> showAlert(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
|
||||
Alert alert = new Alert(alertType, content, buttons);
|
||||
setStageIcon(alert.getDialogPane().getScene().getWindow());
|
||||
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(title);
|
||||
if(graphic != null) {
|
||||
alert.setGraphic(graphic);
|
||||
}
|
||||
|
||||
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
|
||||
Matcher matcher = linkPattern.matcher(content);
|
||||
if(matcher.find()) {
|
||||
String link = matcher.group(1);
|
||||
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(content);
|
||||
hyperlinkLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
hyperlinkLabel.setMaxHeight(Double.MAX_VALUE);
|
||||
hyperlinkLabel.getStyleClass().add("content");
|
||||
Label label = new Label();
|
||||
hyperlinkLabel.setPrefWidth(Math.max(360, TextUtils.computeTextWidth(label.getFont(), link, 0.0D) + 50));
|
||||
hyperlinkLabel.setOnAction(event -> {
|
||||
alert.close();
|
||||
AppServices.get().getApplication().getHostServices().showDocument(link);
|
||||
});
|
||||
alert.getDialogPane().setContent(hyperlinkLabel);
|
||||
}
|
||||
|
||||
String[] lines = content.split("\r\n|\r|\n");
|
||||
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
|
||||
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
|
||||
}
|
||||
|
||||
moveToActiveWindowScreen(alert);
|
||||
return alert.showAndWait();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> requestPassphrase(String walletName, Keystore keystore) {
|
||||
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(walletName, keystore);
|
||||
return passphraseDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface InteractionServices {
|
||||
Optional<ButtonType> showAlert(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons);
|
||||
Optional<String> requestPassphrase(String walletName, Keystore keystore);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
public enum Interface {
|
||||
DESKTOP, TERMINAL, SERVER;
|
||||
|
||||
private static Interface currentInterface;
|
||||
|
||||
public static Interface get() {
|
||||
if(currentInterface == null) {
|
||||
boolean headless = java.awt.GraphicsEnvironment.isHeadless();
|
||||
boolean monocle = "Monocle".equalsIgnoreCase(System.getProperty("glass.platform"));
|
||||
|
||||
if(headless || monocle) {
|
||||
currentInterface = TERMINAL;
|
||||
|
||||
if(headless && !monocle) {
|
||||
throw new UnsupportedOperationException("Headless environment detected but Monocle platform not found");
|
||||
}
|
||||
} else {
|
||||
currentInterface = DESKTOP;
|
||||
}
|
||||
}
|
||||
|
||||
return currentInterface;
|
||||
}
|
||||
|
||||
public static void set(Interface interf) {
|
||||
if(currentInterface != null && interf != currentInterface) {
|
||||
throw new IllegalStateException("Interface already set to " + currentInterface);
|
||||
}
|
||||
|
||||
currentInterface = interf;
|
||||
}
|
||||
}
|
||||
+7
-115
@@ -1,7 +1,5 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
@@ -12,40 +10,27 @@ import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.instance.InstanceException;
|
||||
import com.sparrowwallet.sparrow.instance.InstanceList;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.glyphfont.GlyphFontRegistry;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MainApp extends Application {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.6.6";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
||||
public class SparrowDesktop extends Application {
|
||||
private Stage mainStage;
|
||||
|
||||
private static SparrowInstance sparrowInstance;
|
||||
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||
if(e instanceof IndexOutOfBoundsException && Arrays.stream(e.getStackTrace()).anyMatch(element -> element.getClassName().equals("javafx.scene.chart.BarChart"))) {
|
||||
LoggerFactory.getLogger(MainApp.class).debug("Exception in thread \"" + t.getName() + "\"", e);;
|
||||
LoggerFactory.getLogger(SparrowWallet.class).debug("Exception in thread \"" + t.getName() + "\"", e);;
|
||||
} else {
|
||||
LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e);
|
||||
LoggerFactory.getLogger(SparrowWallet.class).error("Exception in thread \"" + t.getName() + "\"", e);
|
||||
}
|
||||
});
|
||||
super.init();
|
||||
@@ -77,7 +62,7 @@ public class MainApp extends Application {
|
||||
} else if(Network.get() == Network.MAINNET) {
|
||||
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
|
||||
List<PublicElectrumServer> servers = PublicElectrumServer.getServers();
|
||||
Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getUrl());
|
||||
Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getServer());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,102 +114,9 @@ public class MainApp extends Application {
|
||||
Config.get().setAppWidth(mainStage.getWidth());
|
||||
Config.get().setAppHeight(mainStage.getHeight());
|
||||
mainStage.close();
|
||||
if(sparrowInstance != null) {
|
||||
sparrowInstance.freeLock();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] argv) {
|
||||
Args args = new Args();
|
||||
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
|
||||
jCommander.parse(argv);
|
||||
if(args.help) {
|
||||
jCommander.usage();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
if(args.level != null) {
|
||||
Drongo.setRootLogLevel(args.level);
|
||||
}
|
||||
|
||||
if(args.dir != null) {
|
||||
System.setProperty(APP_HOME_PROPERTY, args.dir);
|
||||
getLogger().info("Using configured Sparrow home folder of " + args.dir);
|
||||
}
|
||||
|
||||
if(args.network != null) {
|
||||
Network.set(args.network);
|
||||
} else {
|
||||
String envNetwork = System.getenv(NETWORK_ENV_PROPERTY);
|
||||
if(envNetwork != null) {
|
||||
try {
|
||||
Network.set(Network.valueOf(envNetwork.toUpperCase(Locale.ROOT)));
|
||||
} catch(Exception e) {
|
||||
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
|
||||
if(testnetFlag.exists()) {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||
if(signetFlag.exists()) {
|
||||
Network.set(Network.SIGNET);
|
||||
}
|
||||
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
getLogger().info("Using " + Network.get() + " configuration");
|
||||
}
|
||||
|
||||
List<String> fileUriArguments = jCommander.getUnknownOptions();
|
||||
|
||||
try {
|
||||
sparrowInstance = new SparrowInstance(fileUriArguments);
|
||||
sparrowInstance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
||||
} catch(InstanceException e) {
|
||||
getLogger().error("Could not access application lock", e);
|
||||
}
|
||||
|
||||
if(!fileUriArguments.isEmpty()) {
|
||||
AppServices.parseFileUriArguments(fileUriArguments);
|
||||
}
|
||||
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||
SLF4JBridgeHandler.install();
|
||||
com.sun.javafx.application.LauncherImpl.launchApplication(MainApp.class, MainAppPreloader.class, argv);
|
||||
}
|
||||
|
||||
private static Logger getLogger() {
|
||||
return LoggerFactory.getLogger(MainApp.class);
|
||||
}
|
||||
|
||||
private static class SparrowInstance extends InstanceList {
|
||||
private final List<String> fileUriArguments;
|
||||
|
||||
public SparrowInstance(List<String> fileUriArguments) {
|
||||
super(MainApp.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
|
||||
this.fileUriArguments = fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void receiveMessageList(List<String> messageList) {
|
||||
if(messageList != null && !messageList.isEmpty()) {
|
||||
AppServices.parseFileUriArguments(messageList);
|
||||
AppServices.openFileUriArguments(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> sendMessageList() {
|
||||
return fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeExit() {
|
||||
getLogger().info("Opening files/URIs in already running instance, exiting...");
|
||||
SparrowWallet.Instance instance = SparrowWallet.getSparrowInstance();
|
||||
if(instance != null) {
|
||||
instance.freeLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.instance.InstanceException;
|
||||
import com.sparrowwallet.sparrow.instance.InstanceList;
|
||||
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||
import com.sun.javafx.application.PlatformImpl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public class SparrowWallet {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.7.0";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
||||
private static Instance instance;
|
||||
|
||||
public static void main(String[] argv) {
|
||||
Args args = new Args();
|
||||
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
|
||||
jCommander.parse(argv);
|
||||
if(args.help) {
|
||||
jCommander.usage();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
if(args.version) {
|
||||
System.out.println("Sparrow Wallet " + APP_VERSION);
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
if(args.level != null) {
|
||||
Drongo.setRootLogLevel(args.level);
|
||||
}
|
||||
|
||||
if(args.dir != null) {
|
||||
System.setProperty(APP_HOME_PROPERTY, args.dir);
|
||||
getLogger().info("Using configured Sparrow home folder of " + args.dir);
|
||||
}
|
||||
|
||||
if(args.network != null) {
|
||||
Network.set(args.network);
|
||||
} else {
|
||||
String envNetwork = System.getenv(NETWORK_ENV_PROPERTY);
|
||||
if(envNetwork != null) {
|
||||
try {
|
||||
Network.set(Network.valueOf(envNetwork.toUpperCase(Locale.ROOT)));
|
||||
} catch(Exception e) {
|
||||
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
|
||||
if(testnetFlag.exists()) {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||
if(signetFlag.exists()) {
|
||||
Network.set(Network.SIGNET);
|
||||
}
|
||||
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
getLogger().info("Using " + Network.get() + " configuration");
|
||||
}
|
||||
|
||||
List<String> fileUriArguments = jCommander.getUnknownOptions();
|
||||
|
||||
try {
|
||||
instance = new Instance(fileUriArguments);
|
||||
instance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
||||
} catch(InstanceException e) {
|
||||
getLogger().error("Could not access application lock", e);
|
||||
}
|
||||
|
||||
if(!fileUriArguments.isEmpty()) {
|
||||
AppServices.parseFileUriArguments(fileUriArguments);
|
||||
}
|
||||
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||
SLF4JBridgeHandler.install();
|
||||
|
||||
if(args.terminal) {
|
||||
Interface.set(Interface.TERMINAL);
|
||||
}
|
||||
|
||||
try {
|
||||
if(Interface.get() == Interface.TERMINAL) {
|
||||
PlatformImpl.setTaskbarApplication(false);
|
||||
Drongo.removeRootLogAppender("STDOUT");
|
||||
com.sun.javafx.application.LauncherImpl.launchApplication(SparrowTerminal.class, SparrowWalletPreloader.class, argv);
|
||||
} else {
|
||||
com.sun.javafx.application.LauncherImpl.launchApplication(SparrowDesktop.class, SparrowWalletPreloader.class, argv);
|
||||
}
|
||||
} catch(UnsupportedOperationException e) {
|
||||
Drongo.removeRootLogAppender("STDOUT");
|
||||
getLogger().error("Unable to launch application", e);
|
||||
System.out.println("No display detected. Use Sparrow Server on a headless (no display) system.");
|
||||
|
||||
try {
|
||||
if(instance != null) {
|
||||
instance.freeLock();
|
||||
}
|
||||
} catch(InstanceException instanceException) {
|
||||
getLogger().error("Unable to free instance lock", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Instance getSparrowInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static Logger getLogger() {
|
||||
return LoggerFactory.getLogger(SparrowWallet.class);
|
||||
}
|
||||
|
||||
public static class Instance extends InstanceList {
|
||||
private final List<String> fileUriArguments;
|
||||
|
||||
public Instance(List<String> fileUriArguments) {
|
||||
super(SparrowWallet.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
|
||||
this.fileUriArguments = fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void receiveMessageList(List<String> messageList) {
|
||||
if(messageList != null && !messageList.isEmpty()) {
|
||||
AppServices.parseFileUriArguments(messageList);
|
||||
AppServices.openFileUriArguments(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> sendMessageList() {
|
||||
return fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeExit() {
|
||||
getLogger().info("Opening files/URIs in already running instance, exiting...");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@ package com.sparrowwallet.sparrow;
|
||||
import javafx.application.Preloader;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class MainAppPreloader extends Preloader {
|
||||
public class SparrowWalletPreloader extends Preloader {
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
com.sun.glass.ui.Application.GetApplication().setName("Sparrow");
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum UnitFormat {
|
||||
DOT {
|
||||
private final DecimalFormat btcFormat = new DecimalFormat("0", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
|
||||
public DecimalFormat getBtcFormat() {
|
||||
btcFormat.setMaximumFractionDigits(8);
|
||||
return btcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getTableBtcFormat() {
|
||||
return tableBtcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getCurrencyFormat() {
|
||||
return currencyFormat;
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return Locale.ENGLISH;
|
||||
}
|
||||
},
|
||||
COMMA {
|
||||
private final DecimalFormat btcFormat = new DecimalFormat("0", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
|
||||
public DecimalFormat getBtcFormat() {
|
||||
btcFormat.setMaximumFractionDigits(8);
|
||||
return btcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getTableBtcFormat() {
|
||||
return tableBtcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getCurrencyFormat() {
|
||||
return currencyFormat;
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return Locale.GERMAN;
|
||||
}
|
||||
};
|
||||
|
||||
public abstract Locale getLocale();
|
||||
|
||||
public abstract DecimalFormat getBtcFormat();
|
||||
|
||||
public abstract DecimalFormat getTableBtcFormat();
|
||||
|
||||
public abstract DecimalFormat getCurrencyFormat();
|
||||
|
||||
public String formatBtcValue(Long amount) {
|
||||
return getBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
}
|
||||
|
||||
public String formatSatsValue(Long amount) {
|
||||
return String.format(getLocale(), "%,d", amount);
|
||||
}
|
||||
|
||||
public String formatCurrencyValue(double amount) {
|
||||
return getCurrencyFormat().format(amount);
|
||||
}
|
||||
|
||||
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||
return DecimalFormatSymbols.getInstance(getLocale());
|
||||
}
|
||||
|
||||
public String getGroupingSeparator() {
|
||||
return Character.toString(getDecimalFormatSymbols().getGroupingSeparator());
|
||||
}
|
||||
|
||||
public String getDecimalSeparator() {
|
||||
return Character.toString(getDecimalFormatSymbols().getDecimalSeparator());
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import java.util.*;
|
||||
public class AddressTreeTable extends CoinTreeTable {
|
||||
public void initialize(NodeEntry rootEntry) {
|
||||
getStyleClass().add("address-treetable");
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
String address = rootEntry.getAddress().toString();
|
||||
updateAll(rootEntry);
|
||||
@@ -114,7 +114,7 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
}
|
||||
|
||||
public void updateAll(NodeEntry rootEntry) {
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
setRoot(rootItem);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
@@ -35,8 +36,7 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||
getData().add(balanceSeries);
|
||||
update(walletTransactionsEntry);
|
||||
|
||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||
setBitcoinUnit(walletTransactionsEntry.getWallet(), unit);
|
||||
setUnitFormat(walletTransactionsEntry.getWallet(), Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void update(WalletTransactionsEntry walletTransactionsEntry) {
|
||||
@@ -116,12 +116,16 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||
}
|
||||
}
|
||||
|
||||
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
|
||||
public void setUnitFormat(Wallet wallet, UnitFormat format, BitcoinUnit unit) {
|
||||
if(format == null) {
|
||||
format = UnitFormat.DOT;
|
||||
}
|
||||
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = wallet.getAutoUnit();
|
||||
}
|
||||
|
||||
NumberAxis yaxis = (NumberAxis)getYAxis();
|
||||
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, unit));
|
||||
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.text.ParseException;
|
||||
|
||||
final class CoinAxisFormatter extends StringConverter<Number> {
|
||||
private final UnitFormat unitFormat;
|
||||
private final BitcoinUnit bitcoinUnit;
|
||||
|
||||
public CoinAxisFormatter(NumberAxis axis, BitcoinUnit unit) {
|
||||
public CoinAxisFormatter(NumberAxis axis, UnitFormat format, BitcoinUnit unit) {
|
||||
this.unitFormat = format;
|
||||
this.bitcoinUnit = unit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
Double value = bitcoinUnit.getValue(object.longValue());
|
||||
return CoinTextFormatter.COIN_FORMAT.format(value);
|
||||
return new CoinTextFormatter(unitFormat).getCoinFormat().format(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
try {
|
||||
Number number = CoinTextFormatter.COIN_FORMAT.parse(string);
|
||||
Number number = new CoinTextFormatter(unitFormat).getCoinFormat().parse(string);
|
||||
return bitcoinUnit.getSatsValue(number.doubleValue());
|
||||
} catch (ParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
@@ -14,12 +15,8 @@ import javafx.util.Duration;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
public static final DecimalFormat TABLE_BTC_FORMAT = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
|
||||
private final Tooltip tooltip;
|
||||
|
||||
public CoinCell() {
|
||||
@@ -45,10 +42,11 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
EntryCell.applyRowStyles(this, entry);
|
||||
|
||||
CoinTreeTable coinTreeTable = (CoinTreeTable)getTreeTableView();
|
||||
UnitFormat format = coinTreeTable.getUnitFormat();
|
||||
BitcoinUnit unit = coinTreeTable.getBitcoinUnit();
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", amount.longValue());
|
||||
DecimalFormat decimalFormat = (amount.longValue() == 0L ? CoinLabel.getBTCFormat() : TABLE_BTC_FORMAT);
|
||||
String satsValue = format.formatSatsValue(amount.longValue());
|
||||
DecimalFormat decimalFormat = (amount.longValue() == 0L ? format.getBtcFormat() : format.getTableBtcFormat());
|
||||
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
@@ -61,8 +59,7 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
setTooltip(tooltip);
|
||||
String tooltipValue = tooltip.getText();
|
||||
|
||||
if(entry instanceof TransactionEntry) {
|
||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
tooltip.setText(tooltipValue + " (" + transactionEntry.getConfirmationsDescription() + ")");
|
||||
|
||||
transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
@@ -12,13 +12,7 @@ import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
public class CoinLabel extends Label {
|
||||
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
private final CoinContextMenu contextMenu;
|
||||
@@ -58,8 +52,9 @@ public class CoinLabel extends Label {
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
|
||||
String btcValue = BTC_FORMAT.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
String satsValue = format.formatSatsValue(value) + " sats";
|
||||
String btcValue = format.formatBtcValue(value) + " BTC";
|
||||
|
||||
BitcoinUnit unit = bitcoinUnit;
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
@@ -89,16 +84,12 @@ public class CoinLabel extends Label {
|
||||
copyBtcValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(BTC_FORMAT.format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
content.putString(format.formatBtcValue(getValue()));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(copySatsValue, copyBtcValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static DecimalFormat getBTCFormat() {
|
||||
BTC_FORMAT.setMaximumFractionDigits(8);
|
||||
return BTC_FORMAT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.text.ParseException;
|
||||
import java.util.Locale;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CoinTextFormatter extends TextFormatter<String> {
|
||||
private static final Pattern COIN_VALIDATION = Pattern.compile("[\\d,]*(\\.\\d{0,8})?");
|
||||
public static final DecimalFormat COIN_FORMAT = new DecimalFormat("###,###.########", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
public CoinTextFormatter(UnitFormat unitFormat) {
|
||||
super(new CoinFilter(unitFormat));
|
||||
}
|
||||
|
||||
public CoinTextFormatter() {
|
||||
super(new CoinFilter());
|
||||
public UnitFormat getUnitFormat() {
|
||||
return ((CoinFilter)getFilter()).unitFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getCoinFormat() {
|
||||
return ((CoinFilter)getFilter()).coinFormat;
|
||||
}
|
||||
|
||||
private static class CoinFilter implements UnaryOperator<Change> {
|
||||
private final UnitFormat unitFormat;
|
||||
private final DecimalFormat coinFormat;
|
||||
private final Pattern coinValidation;
|
||||
|
||||
public CoinFilter(UnitFormat unitFormat) {
|
||||
this.unitFormat = unitFormat == null ? UnitFormat.DOT : unitFormat;
|
||||
this.coinFormat = new DecimalFormat("###,###.########", DecimalFormatSymbols.getInstance(unitFormat.getLocale()));
|
||||
this.coinValidation = Pattern.compile("[\\d" + Pattern.quote(unitFormat.getGroupingSeparator()) + "]*(" + Pattern.quote(unitFormat.getDecimalSeparator()) + "\\d{0,8})?");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Change apply(Change change) {
|
||||
String oldText = change.getControlText();
|
||||
@@ -30,17 +45,17 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||
|
||||
String noFractionCommaText = newText;
|
||||
int commasRemoved = 0;
|
||||
int dotIndex = newText.indexOf(".");
|
||||
int dotIndex = newText.indexOf(unitFormat.getDecimalSeparator());
|
||||
if(dotIndex > -1) {
|
||||
noFractionCommaText = newText.substring(0, dotIndex) + newText.substring(dotIndex).replaceAll(",", "");
|
||||
noFractionCommaText = newText.substring(0, dotIndex) + newText.substring(dotIndex).replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "");
|
||||
commasRemoved = newText.length() - noFractionCommaText.length();
|
||||
}
|
||||
|
||||
if(!COIN_VALIDATION.matcher(noFractionCommaText).matches()) {
|
||||
if(!coinValidation.matcher(noFractionCommaText).matches()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(",".equals(change.getText())) {
|
||||
if(unitFormat.getGroupingSeparator().equals(change.getText())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -48,20 +63,20 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||
return change;
|
||||
}
|
||||
|
||||
if(change.isDeleted() && ",".equals(deleted) && change.getRangeStart() > 0) {
|
||||
if(change.isDeleted() && unitFormat.getGroupingSeparator().equals(deleted) && change.getRangeStart() > 0) {
|
||||
noFractionCommaText = noFractionCommaText.substring(0, change.getRangeStart() - 1) + noFractionCommaText.substring(change.getRangeEnd() - 1);
|
||||
}
|
||||
|
||||
try {
|
||||
Number value = COIN_FORMAT.parse(noFractionCommaText);
|
||||
String correct = COIN_FORMAT.format(value.doubleValue());
|
||||
Number value = coinFormat.parse(noFractionCommaText);
|
||||
String correct = coinFormat.format(value.doubleValue());
|
||||
|
||||
String compare = newText;
|
||||
if(compare.contains(".") && compare.endsWith("0")) {
|
||||
if(compare.contains(unitFormat.getDecimalSeparator()) && compare.endsWith("0")) {
|
||||
compare = compare.replaceAll("0*$", "");
|
||||
}
|
||||
|
||||
if(compare.endsWith(".")) {
|
||||
if(compare.endsWith(unitFormat.getDecimalSeparator())) {
|
||||
compare = compare.substring(0, compare.length() - 1);
|
||||
}
|
||||
|
||||
@@ -79,11 +94,11 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||
|
||||
if(correct.length() != newText.length()) {
|
||||
String postCorrect = correct.substring(Math.min(change.getCaretPosition(), correct.length()));
|
||||
int commasAfter = postCorrect.length() - postCorrect.replace(",", "").length();
|
||||
int caretShift = change.isDeleted() && ".".equals(deleted) ? commasAfter : 0;
|
||||
int commasAfter = postCorrect.length() - postCorrect.replace(unitFormat.getGroupingSeparator(), "").length();
|
||||
int caretShift = change.isDeleted() && unitFormat.getDecimalSeparator().equals(deleted) ? commasAfter : 0;
|
||||
|
||||
int caret = change.getCaretPosition() + (correct.length() - newText.length() - caretShift) + commasRemoved;
|
||||
if(caret >= 0) {
|
||||
if(caret >= 0 && caret <= change.getControlNewText().length()) {
|
||||
change.setCaretPosition(caret);
|
||||
change.setAnchor(caret);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||
@@ -26,25 +27,36 @@ import java.util.Optional;
|
||||
|
||||
public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
private UnitFormat unitFormat;
|
||||
|
||||
public BitcoinUnit getBitcoinUnit() {
|
||||
return bitcoinUnit;
|
||||
}
|
||||
|
||||
public void setBitcoinUnit(BitcoinUnit bitcoinUnit) {
|
||||
this.bitcoinUnit = bitcoinUnit;
|
||||
public UnitFormat getUnitFormat() {
|
||||
return unitFormat;
|
||||
}
|
||||
|
||||
public void setBitcoinUnit(Wallet wallet) {
|
||||
setBitcoinUnit(wallet, Config.get().getBitcoinUnit());
|
||||
public void setUnitFormat(Wallet wallet) {
|
||||
setUnitFormat(wallet, Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
|
||||
public void setUnitFormat(Wallet wallet, UnitFormat format) {
|
||||
setUnitFormat(wallet, format, Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void setUnitFormat(Wallet wallet, UnitFormat format, BitcoinUnit unit) {
|
||||
if(format == null) {
|
||||
format = UnitFormat.DOT;
|
||||
}
|
||||
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = wallet.getAutoUnit();
|
||||
}
|
||||
|
||||
boolean changed = (bitcoinUnit != unit);
|
||||
boolean changed = (unitFormat != format);
|
||||
changed |= (bitcoinUnit != unit);
|
||||
this.unitFormat = format;
|
||||
this.bitcoinUnit = unit;
|
||||
|
||||
if(changed && !getChildren().isEmpty()) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
@@ -11,8 +11,6 @@ import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class CopyableCoinLabel extends CopyableLabel {
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
@@ -24,7 +22,7 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
|
||||
public CopyableCoinLabel(String text) {
|
||||
super(text);
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new CoinContextMenu();
|
||||
}
|
||||
@@ -42,19 +40,23 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
refresh(Config.get().getBitcoinUnit());
|
||||
refresh(Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void refresh(BitcoinUnit bitcoinUnit) {
|
||||
setValueAsText(getValue(), bitcoinUnit);
|
||||
public void refresh(UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
|
||||
setValueAsText(getValue(), unitFormat, bitcoinUnit);
|
||||
}
|
||||
|
||||
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
|
||||
private void setValueAsText(Long value, UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
|
||||
String btcValue = CoinLabel.getBTCFormat().format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
|
||||
if(unitFormat == null) {
|
||||
unitFormat = UnitFormat.DOT;
|
||||
}
|
||||
|
||||
String satsValue = unitFormat.formatSatsValue(value) + " sats";
|
||||
String btcValue = unitFormat.formatBtcValue(value) + " BTC";
|
||||
|
||||
BitcoinUnit unit = bitcoinUnit;
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
@@ -84,7 +86,8 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
copyBtcValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(CoinLabel.getBTCFormat().format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
content.putString(format.formatBtcValue(getValue()));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.lowagie.text.*;
|
||||
import com.lowagie.text.Font;
|
||||
import com.lowagie.text.Image;
|
||||
import com.lowagie.text.pdf.PdfWriter;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.UREncoder;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.PdfUtils;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
private static final Logger log = LoggerFactory.getLogger(DescriptorQRDisplayDialog.class);
|
||||
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
|
||||
super(ur);
|
||||
|
||||
@@ -31,43 +16,11 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
dialogPane.getButtonTypes().add(pdfButtonType);
|
||||
|
||||
Button pdfButton = (Button)dialogPane.lookupButton(pdfButtonType);
|
||||
pdfButton.setGraphicTextGap(5);
|
||||
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
||||
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
savePdf(walletName, outputDescriptor, ur);
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur);
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
private void savePdf(String walletName, String outputDescriptor, UR ur) {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save PDF");
|
||||
fileChooser.setInitialFileName(walletName + ".pdf");
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
try(Document document = new Document()) {
|
||||
document.setMargins(36, 36, 48, 36);
|
||||
PdfWriter.getInstance(document, new FileOutputStream(file));
|
||||
document.open();
|
||||
|
||||
Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16, Color.BLACK);
|
||||
Chunk title = new Chunk("Output descriptor for " + walletName, titleFont);
|
||||
document.add(title);
|
||||
|
||||
UREncoder urEncoder = new UREncoder(ur, 2000, 10, 0);
|
||||
String fragment = urEncoder.nextPart();
|
||||
if(urEncoder.isSinglePart()) {
|
||||
Image image = Image.getInstance(SwingFXUtils.fromFXImage(getQrCode(fragment), null), Color.WHITE);
|
||||
document.add(image);
|
||||
}
|
||||
|
||||
Font descriptorFont = FontFactory.getFont(FontFactory.COURIER, 14, Color.BLACK);
|
||||
Paragraph descriptor = new Paragraph(outputDescriptor, descriptorFont);
|
||||
document.add(descriptor);
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating descriptor PDF", e);
|
||||
AppServices.showErrorDialog("Error creating PDF", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -96,11 +96,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
actionBox.getChildren().add(viewTransactionButton);
|
||||
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() &&
|
||||
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
Button increaseFeeButton = new Button("");
|
||||
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
increaseFeeButton.setOnAction(event -> {
|
||||
increaseFee(transactionEntry);
|
||||
increaseFee(transactionEntry, false);
|
||||
});
|
||||
actionBox.getChildren().add(increaseFeeButton);
|
||||
}
|
||||
@@ -188,7 +189,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
}
|
||||
|
||||
private static void increaseFee(TransactionEntry transactionEntry) {
|
||||
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
||||
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
||||
@@ -200,6 +201,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if(utxos.isEmpty()) {
|
||||
log.error("No UTXOs to replace");
|
||||
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction - no replaceable UTXOs were found.");
|
||||
return;
|
||||
}
|
||||
|
||||
List<TransactionOutput> ourOutputs = transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
@@ -222,7 +229,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
//Remove any UTXOs created by the transaction that is to be replaced
|
||||
walletUtxos.removeIf(utxo -> ourOutputs.stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex()));
|
||||
Collections.shuffle(walletUtxos);
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty()) {
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty() && !cancelTransaction) {
|
||||
//If there is insufficient change output, include another random UTXO so the fee can be increased
|
||||
BlockTransactionHashIndex utxo = walletUtxos.remove(0);
|
||||
utxos.add(utxo);
|
||||
@@ -279,6 +286,15 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
return;
|
||||
}
|
||||
|
||||
if(cancelTransaction) {
|
||||
Payment existing = payments.get(0);
|
||||
Address address = transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress();
|
||||
Payment payment = new Payment(address, existing.getLabel(), existing.getAmount(), true);
|
||||
payments.clear();
|
||||
payments.add(payment);
|
||||
opReturns.clear();
|
||||
}
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, blockTransaction.getFee(), true)));
|
||||
}
|
||||
@@ -410,6 +426,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
return increaseFeeGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getCancelTransactionRBFGlyph() {
|
||||
Glyph cancelTxGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BAN);
|
||||
cancelTxGlyph.setFontSize(12);
|
||||
return cancelTxGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getIncreaseFeeCPFPGlyph() {
|
||||
Glyph cpfpGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SIGN_OUT_ALT);
|
||||
cpfpGlyph.setFontSize(12);
|
||||
@@ -463,17 +485,28 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
});
|
||||
getItems().add(viewTransaction);
|
||||
|
||||
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
if(blockTransaction.getTransaction().isReplaceByFee() && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
increaseFee.setOnAction(AE -> {
|
||||
hide();
|
||||
increaseFee(transactionEntry);
|
||||
increaseFee(transactionEntry, false);
|
||||
});
|
||||
|
||||
getItems().add(increaseFee);
|
||||
}
|
||||
|
||||
if(blockTransaction.getTransaction().isReplaceByFee() && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
||||
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
||||
cancelTx.setOnAction(AE -> {
|
||||
hide();
|
||||
increaseFee(transactionEntry, true);
|
||||
});
|
||||
|
||||
getItems().add(cancelTx);
|
||||
}
|
||||
|
||||
if(containsWalletOutputs(transactionEntry)) {
|
||||
MenuItem createCpfp = new MenuItem("Increase Effective Fee (CPFP)");
|
||||
createCpfp.setGraphic(getIncreaseFeeCPFPGlyph());
|
||||
@@ -558,6 +591,42 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
getItems().add(signVerifyMessage);
|
||||
}
|
||||
|
||||
if(nodeEntry != null && !nodeEntry.getNode().getUnspentTransactionOutputs().isEmpty()) {
|
||||
List<BlockTransactionHashIndex> utxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().collect(Collectors.toList());
|
||||
MenuItem spendUtxos = new MenuItem("Spend UTXOs");
|
||||
spendUtxos.setGraphic(getSendGlyph());
|
||||
spendUtxos.setOnAction(AE -> {
|
||||
hide();
|
||||
EventManager.get().post(new SendActionEvent(nodeEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(nodeEntry.getWallet(), utxos)));
|
||||
});
|
||||
getItems().add(spendUtxos);
|
||||
|
||||
List<BlockTransactionHashIndex> unfrozenUtxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().filter(utxo -> utxo.getStatus() != Status.FROZEN).collect(Collectors.toList());
|
||||
if(!unfrozenUtxos.isEmpty()) {
|
||||
MenuItem freezeUtxos = new MenuItem("Freeze UTXOs");
|
||||
freezeUtxos.setGraphic(getFreezeGlyph());
|
||||
freezeUtxos.setOnAction(AE -> {
|
||||
hide();
|
||||
unfrozenUtxos.forEach(utxo -> utxo.setStatus(Status.FROZEN));
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(nodeEntry.getWallet(), unfrozenUtxos));
|
||||
});
|
||||
getItems().add(freezeUtxos);
|
||||
}
|
||||
|
||||
List<BlockTransactionHashIndex> frozenUtxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().filter(utxo -> utxo.getStatus() == Status.FROZEN).collect(Collectors.toList());
|
||||
if(!frozenUtxos.isEmpty()) {
|
||||
MenuItem unfreezeUtxos = new MenuItem("Unfreeze UTXOs");
|
||||
unfreezeUtxos.setGraphic(getUnfreezeGlyph());
|
||||
unfreezeUtxos.setOnAction(AE -> {
|
||||
hide();
|
||||
frozenUtxos.forEach(utxo -> utxo.setStatus(null));
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(nodeEntry.getWallet(), frozenUtxos));
|
||||
});
|
||||
getItems().add(unfreezeUtxos);
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem copyAddress = new MenuItem("Copy Address");
|
||||
copyAddress.setOnAction(AE -> {
|
||||
hide();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
@@ -10,14 +12,9 @@ import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Currency;
|
||||
import java.util.Locale;
|
||||
|
||||
public class FiatLabel extends CopyableLabel {
|
||||
private static final DecimalFormat CURRENCY_FORMAT = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final DoubleProperty btcRateProperty = new SimpleDoubleProperty(0.0);
|
||||
private final ObjectProperty<Currency> currencyProperty = new SimpleObjectProperty<>(null);
|
||||
@@ -30,9 +27,9 @@ public class FiatLabel extends CopyableLabel {
|
||||
|
||||
public FiatLabel(String text) {
|
||||
super(text);
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue));
|
||||
btcRateProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue()));
|
||||
currencyProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue()));
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat()));
|
||||
btcRateProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue(), Config.get().getUnitFormat()));
|
||||
currencyProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue(), Config.get().getUnitFormat()));
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new FiatContextMenu();
|
||||
}
|
||||
@@ -83,14 +80,22 @@ public class FiatLabel extends CopyableLabel {
|
||||
setCurrency(currency);
|
||||
}
|
||||
|
||||
private void setValueAsText(long balance) {
|
||||
public void refresh() {
|
||||
refresh(Config.get().getUnitFormat());
|
||||
}
|
||||
|
||||
public void refresh(UnitFormat unitFormat) {
|
||||
setValueAsText(getValue(), unitFormat);
|
||||
}
|
||||
|
||||
private void setValueAsText(long balance, UnitFormat unitFormat) {
|
||||
if(getCurrency() != null && getBtcRate() > 0.0) {
|
||||
BigDecimal satsBalance = BigDecimal.valueOf(balance);
|
||||
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
|
||||
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(getBtcRate()));
|
||||
|
||||
String label = getCurrency().getSymbol() + " " + CURRENCY_FORMAT.format(fiatBalance.doubleValue());
|
||||
tooltip.setText("1 BTC = " + getCurrency().getSymbol() + " " + CURRENCY_FORMAT.format(getBtcRate()));
|
||||
String label = getCurrency().getSymbol() + " " + unitFormat.formatCurrencyValue(fiatBalance.doubleValue());
|
||||
tooltip.setText("1 BTC = " + getCurrency().getSymbol() + " " + unitFormat.formatCurrencyValue(getBtcRate()));
|
||||
|
||||
setText(label);
|
||||
setTooltip(tooltip);
|
||||
|
||||
@@ -82,7 +82,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
|
||||
String extension = exporter.getExportFileExtension(wallet);
|
||||
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase(Locale.ROOT).replace(" ", "");
|
||||
if(exporter instanceof Sparrow) {
|
||||
if(exporter.exportsAllWallets()) {
|
||||
fileName = wallet.getMasterName();
|
||||
}
|
||||
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
|
||||
|
||||
@@ -121,6 +121,10 @@ public class PayNymAvatar extends StackPane {
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
String url = PayNymService.getHostUrl(proxy != null) + "/" + paymentCodeStr + "/avatar";
|
||||
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("Requesting PayNym avatar from " + url);
|
||||
}
|
||||
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
|
||||
Image image = new Image(is, 150, 150, true, false);
|
||||
paymentCodeCache.put(cacheId, image);
|
||||
|
||||
@@ -244,11 +244,11 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
protected static Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
results = new CoinTreeTable();
|
||||
results.setShowRoot(false);
|
||||
results.setPrefWidth(showWallet ? 950 : 850);
|
||||
results.setBitcoinUnit(walletForms.iterator().next().getWallet());
|
||||
results.setUnitFormat(walletForms.iterator().next().getWallet());
|
||||
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
results.setPlaceholder(new Label("No results"));
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
@@ -21,6 +21,7 @@ import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -28,15 +29,17 @@ import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.CubicCurve;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
@@ -45,11 +48,14 @@ import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.control.CoinLabel.BTC_FORMAT;
|
||||
|
||||
public class TransactionDiagram extends GridPane {
|
||||
private static final int MAX_UTXOS = 8;
|
||||
private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 2;
|
||||
@@ -69,13 +75,14 @@ public class TransactionDiagram extends GridPane {
|
||||
private final ObjectProperty<OptimizationStrategy> optimizationStrategyProperty = new SimpleObjectProperty<>(OptimizationStrategy.EFFICIENCY);
|
||||
private boolean expanded;
|
||||
private TransactionDiagram expandedDiagram;
|
||||
private ContextMenu contextMenu;
|
||||
|
||||
private final EventHandler<MouseEvent> expandedDiagramHandler = new EventHandler<>() {
|
||||
@Override
|
||||
public void handle(MouseEvent event) {
|
||||
if(!event.isConsumed() && event.getButton() != MouseButton.SECONDARY) {
|
||||
Stage stage = new Stage(StageStyle.UNDECORATED);
|
||||
stage.setTitle(walletTx.getPayments().iterator().next().getLabel());
|
||||
stage.setTitle(getDiagramTitle());
|
||||
stage.initOwner(TransactionDiagram.this.getScene().getWindow());
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.setResizable(false);
|
||||
@@ -98,7 +105,8 @@ public class TransactionDiagram extends GridPane {
|
||||
expandedDiagram = new TransactionDiagram();
|
||||
expandedDiagram.setId("transactionDiagram");
|
||||
expandedDiagram.setExpanded(true);
|
||||
updateExpandedDiagram();
|
||||
expandedDiagram.setFinal(isFinal());
|
||||
updateDerivedDiagram(expandedDiagram);
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
buttonBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -125,6 +133,13 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
};
|
||||
|
||||
private final EventHandler<ContextMenuEvent> contextMenuHandler = new EventHandler<>() {
|
||||
@Override
|
||||
public void handle(ContextMenuEvent event) {
|
||||
contextMenu.show(getChildren().iterator().next(), event.getScreenX(), event.getScreenY());
|
||||
}
|
||||
};
|
||||
|
||||
public void update(WalletTransaction walletTx) {
|
||||
setMinHeight(getDiagramHeight());
|
||||
setMaxHeight(getDiagramHeight());
|
||||
@@ -136,7 +151,7 @@ public class TransactionDiagram extends GridPane {
|
||||
update();
|
||||
setOnMouseClicked(expandedDiagramHandler);
|
||||
if(expandedDiagram != null) {
|
||||
updateExpandedDiagram();
|
||||
updateDerivedDiagram(expandedDiagram);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,20 +180,22 @@ public class TransactionDiagram extends GridPane {
|
||||
getChildren().clear();
|
||||
}
|
||||
|
||||
private void updateExpandedDiagram() {
|
||||
expandedDiagram.setFinal(isFinal());
|
||||
expandedDiagram.setOptimizationStrategy(getOptimizationStrategy());
|
||||
expandedDiagram.walletTx = walletTx;
|
||||
private void updateDerivedDiagram(TransactionDiagram diagram) {
|
||||
diagram.setOptimizationStrategy(getOptimizationStrategy());
|
||||
diagram.walletTx = walletTx;
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> utxoSets = expandedDiagram.getDisplayedUtxoSets();
|
||||
int maxSetSize = utxoSets.stream().mapToInt(Map::size).max().orElse(0);
|
||||
int maxRows = Math.max(maxSetSize * utxoSets.size(), walletTx.getPayments().size() + 2);
|
||||
double diagramHeight = Math.max(DIAGRAM_HEIGHT, Math.min(EXPANDED_DIAGRAM_HEIGHT, maxRows * ROW_HEIGHT));
|
||||
expandedDiagram.setMinHeight(diagramHeight);
|
||||
expandedDiagram.setMaxHeight(diagramHeight);
|
||||
expandedDiagram.update();
|
||||
if(diagram.isExpanded()) {
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> utxoSets = diagram.getDisplayedUtxoSets();
|
||||
int maxSetSize = utxoSets.stream().mapToInt(Map::size).max().orElse(0);
|
||||
int maxRows = Math.max(maxSetSize * utxoSets.size(), walletTx.getPayments().size() + 2);
|
||||
double diagramHeight = Math.max(DIAGRAM_HEIGHT, Math.min(EXPANDED_DIAGRAM_HEIGHT, maxRows * ROW_HEIGHT));
|
||||
diagram.setMinHeight(diagramHeight);
|
||||
diagram.setMaxHeight(diagramHeight);
|
||||
}
|
||||
|
||||
if(expandedDiagram.getScene() != null && expandedDiagram.getScene().getWindow() instanceof Stage stage) {
|
||||
diagram.update();
|
||||
|
||||
if(diagram.getScene() != null && diagram.getScene().getWindow() instanceof Stage stage) {
|
||||
stage.sizeToScene();
|
||||
}
|
||||
}
|
||||
@@ -208,6 +225,17 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
getChildren().clear();
|
||||
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
||||
|
||||
if(contextMenu == null) {
|
||||
contextMenu = new ContextMenu();
|
||||
MenuItem menuItem = new MenuItem("Save as Image...");
|
||||
menuItem.setOnAction(event -> {
|
||||
contextMenu.hide();
|
||||
saveAsImage();
|
||||
});
|
||||
contextMenu.getItems().add(menuItem);
|
||||
setOnContextMenuRequested(contextMenuHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
|
||||
@@ -434,7 +462,7 @@ public class TransactionDiagram extends GridPane {
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
joiner.add("Spending " + getSatsValue(inputValue) + " sats from" + (isExpanded() ? ":" : " (click to expand):"));
|
||||
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
|
||||
joiner.add(additionalInput.getHashAsString() + ":" + additionalInput.getIndex());
|
||||
joiner.add(getInputDescription(additionalInput));
|
||||
}
|
||||
tooltip.setText(joiner.toString());
|
||||
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
|
||||
@@ -797,12 +825,51 @@ public class TransactionDiagram extends GridPane {
|
||||
tooltip.setShowDuration(Duration.INDEFINITE);
|
||||
tooltip.getStyleClass().add("transaction-tooltip");
|
||||
txLabel.setTooltip(tooltip);
|
||||
|
||||
txPane.getChildren().add(txLabel);
|
||||
txPane.getChildren().add(createSpacer());
|
||||
|
||||
return txPane;
|
||||
}
|
||||
|
||||
private void saveAsImage() {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save Image");
|
||||
fileChooser.setInitialFileName(getDiagramTitle() + ".png");
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
TransactionDiagram transactionDiagram = new TransactionDiagram();
|
||||
transactionDiagram.setId("transactionDiagram");
|
||||
transactionDiagram.setFinal(true);
|
||||
transactionDiagram.setExpanded(isExpanded());
|
||||
transactionDiagram.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
|
||||
transactionDiagram.setStyle("-fx-text-background-color: " + (Config.get().getTheme() == Theme.DARK ? "#ffffff" : "#000000"));
|
||||
updateDerivedDiagram(transactionDiagram);
|
||||
Scene scene = new Scene(transactionDiagram);
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
scene.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
scene.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||
scene.getStylesheets().add(AppServices.class.getResource("wallet/send.css").toExternalForm());
|
||||
Image image = scene.snapshot(null);
|
||||
BufferedImage bufferedImage = SwingFXUtils.fromFXImage(image, null);
|
||||
try {
|
||||
ImageIO.write(bufferedImage, "png", file);
|
||||
} catch(IOException e) {
|
||||
AppServices.showErrorDialog("Error saving image", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getDiagramTitle() {
|
||||
if(!isFinal() && walletTx.getPayments().size() > 0 && walletTx.getPayments().get(0).getLabel() != null) {
|
||||
return walletTx.getPayments().get(0).getLabel();
|
||||
} else {
|
||||
return "[" + walletTx.getTransaction().getTxId().toString().substring(0, 6) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public double getDiagramHeight() {
|
||||
if(isExpanded()) {
|
||||
return getMaxHeight();
|
||||
@@ -1244,6 +1311,14 @@ public class TransactionDiagram extends GridPane {
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyAddress);
|
||||
|
||||
MenuItem showAddress = new MenuItem("Show Address as QR...");
|
||||
showAddress.setOnAction(event -> {
|
||||
hide();
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(address.toString());
|
||||
qrDisplayDialog.showAndWait();
|
||||
});
|
||||
getItems().add(showAddress);
|
||||
}
|
||||
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||
@@ -1257,7 +1332,8 @@ public class TransactionDiagram extends GridPane {
|
||||
copyBtcValue.setOnAction(event -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(BTC_FORMAT.format((double)value / Transaction.SATOSHIS_PER_BITCOIN));
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
content.putString(format.formatBtcValue(value));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().addAll(copySatsValue, copyBtcValue);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
||||
@@ -8,12 +7,10 @@ import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TransactionsTreeTable extends CoinTreeTable {
|
||||
public void initialize(WalletTransactionsEntry rootEntry) {
|
||||
getStyleClass().add("transactions-treetable");
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
updateAll(rootEntry);
|
||||
setShowRoot(false);
|
||||
@@ -58,7 +55,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
|
||||
}
|
||||
|
||||
public void updateAll(WalletTransactionsEntry rootEntry) {
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
setRoot(rootItem);
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
@@ -36,8 +37,7 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
getData().add(utxoSeries);
|
||||
update(walletUtxosEntry);
|
||||
|
||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||
setBitcoinUnit(walletUtxosEntry.getWallet(), unit);
|
||||
setUnitFormat(walletUtxosEntry.getWallet(), Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void update(WalletUtxosEntry walletUtxosEntry) {
|
||||
@@ -117,12 +117,16 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
this.selectedEntries = entries;
|
||||
}
|
||||
|
||||
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
|
||||
public void setUnitFormat(Wallet wallet, UnitFormat format, BitcoinUnit unit) {
|
||||
if(format == null) {
|
||||
format = UnitFormat.DOT;
|
||||
}
|
||||
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = wallet.getAutoUnit();
|
||||
}
|
||||
|
||||
NumberAxis yaxis = (NumberAxis)getYAxis();
|
||||
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, unit));
|
||||
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.TableColumnBase;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class UtxosTreeTable extends CoinTreeTable {
|
||||
public void initialize(WalletUtxosEntry rootEntry) {
|
||||
getStyleClass().add("utxos-treetable");
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
updateAll(rootEntry);
|
||||
setShowRoot(false);
|
||||
@@ -93,7 +90,7 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
}
|
||||
|
||||
public void updateAll(WalletUtxosEntry rootEntry) {
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
setRoot(rootItem);
|
||||
|
||||
@@ -42,17 +42,19 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
|
||||
List<WalletExport> exporters;
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
exporters = List.of(new Electrum(), new Descriptor(), new SpecterDesktop(), new Sparrow());
|
||||
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow());
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
|
||||
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(), new Descriptor(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||
}
|
||||
|
||||
Accordion exportAccordion = new Accordion();
|
||||
for (WalletExport exporter : exporters) {
|
||||
FileWalletExportPane exportPane = new FileWalletExportPane(wallet, exporter);
|
||||
exportAccordion.getPanes().add(exportPane);
|
||||
for(WalletExport exporter : exporters) {
|
||||
if(!exporter.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileWalletExportPane exportPane = new FileWalletExportPane(wallet, exporter);
|
||||
exportAccordion.getPanes().add(exportPane);
|
||||
}
|
||||
}
|
||||
|
||||
exportAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||
|
||||
@@ -49,16 +49,20 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
importAccordion = new Accordion();
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY());
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY());
|
||||
for(KeystoreFileImport importer : keystoreImporters) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
}
|
||||
}
|
||||
|
||||
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
|
||||
for(WalletImport importer : walletImporters) {
|
||||
FileWalletImportPane importPane = new FileWalletImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileWalletImportPane importPane = new FileWalletImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
}
|
||||
}
|
||||
|
||||
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
|
||||
public class BitcoinUnitChangedEvent {
|
||||
public class BitcoinUnitChangedEvent extends UnitFormatChangedEvent {
|
||||
private final BitcoinUnit bitcoinUnit;
|
||||
|
||||
public BitcoinUnitChangedEvent(BitcoinUnit bitcoinUnit) {
|
||||
super(Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat());
|
||||
this.bitcoinUnit = bitcoinUnit;
|
||||
}
|
||||
|
||||
public BitcoinUnitChangedEvent(UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
|
||||
super(unitFormat);
|
||||
this.bitcoinUnit = bitcoinUnit;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.control.CoinLabel;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
@@ -13,7 +12,6 @@ import com.sparrowwallet.sparrow.wallet.TransactionHashIndexEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class NewWalletTransactionsEvent {
|
||||
@@ -50,16 +48,21 @@ public class NewWalletTransactionsEvent {
|
||||
}
|
||||
|
||||
public String getValueAsText(long value) {
|
||||
UnitFormat format = Config.get().getUnitFormat();
|
||||
if(format == null) {
|
||||
format = UnitFormat.DOT;
|
||||
}
|
||||
|
||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
|
||||
}
|
||||
|
||||
if(unit == BitcoinUnit.BTC) {
|
||||
return CoinLabel.getBTCFormat().format((double) value / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
|
||||
return format.formatBtcValue(value) + " BTC";
|
||||
}
|
||||
|
||||
return String.format(Locale.ENGLISH, "%,d", value) + " sats";
|
||||
return format.formatSatsValue(value) + " sats";
|
||||
}
|
||||
|
||||
public List<BlockTransaction> getUnspentConfirmingWhirlpoolMixTransactions() {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
|
||||
public class UnitFormatChangedEvent {
|
||||
private final UnitFormat unitFormat;
|
||||
|
||||
public UnitFormatChangedEvent(UnitFormat unitFormat) {
|
||||
this.unitFormat = unitFormat;
|
||||
}
|
||||
|
||||
public UnitFormat getUnitFormat() {
|
||||
return unitFormat;
|
||||
}
|
||||
|
||||
public BitcoinUnit getBitcoinUnit() {
|
||||
return Config.get().getBitcoinUnit();
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||
FEATHER_ALT('\uf56b'),
|
||||
FILE_CSV('\uf6dd'),
|
||||
FILE_IMPORT('\uf56f'),
|
||||
FILE_PDF('\uf1c1'),
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||
HAND_HOLDING_WATER('\uf4c1'),
|
||||
@@ -67,6 +68,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||
SNOWFLAKE('\uf2dc'),
|
||||
SORT_NUMERIC_DOWN('\uf162'),
|
||||
SUN('\uf185'),
|
||||
TAG('\uf02b'),
|
||||
THEATER_MASKS('\uf630'),
|
||||
TIMES_CIRCLE('\uf057'),
|
||||
TOGGLE_OFF('\uf204'),
|
||||
|
||||
@@ -67,4 +67,9 @@ public class CoboVaultMultisig extends ColdcardMultisig {
|
||||
public boolean isWalletExportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeprecated() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,11 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDeprecated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static class CoboVaultSinglesigKeystore {
|
||||
public String ExtPubKey;
|
||||
public String MasterFingerprint;
|
||||
|
||||
@@ -187,7 +187,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
writer.append("# " + getName() + " setup file (created by Sparrow)\n");
|
||||
writer.append("#\n");
|
||||
writer.append("Name: ").append(wallet.getFullName()).append("\n");
|
||||
writer.append("Name: ").append(wallet.getFullName().length() >= 20 ? (wallet.getDisplayName().length() >= 20 ? wallet.getDisplayName().substring(0, 20) : wallet.getDisplayName()) : wallet.getFullName()).append("\n");
|
||||
writer.append("Policy: ").append(Integer.toString(wallet.getDefaultPolicy().getNumSignaturesRequired())).append(" of ").append(Integer.toString(wallet.getKeystores().size())).append("\n");
|
||||
if(!multipleDerivations) {
|
||||
writer.append("Derivation: ").append(wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath()).append("\n");
|
||||
@@ -214,7 +214,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "Export file that can be read by your Coldcard using the Settings > Multisig Wallets > Import from SD feature.";
|
||||
return "Export file that can be read by your Coldcard using the Settings > Multisig Wallets > Import from File feature.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.Mode;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
@@ -27,6 +28,7 @@ public class Config {
|
||||
|
||||
private Mode mode;
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
private UnitFormat unitFormat;
|
||||
private FeeRatesSource feeRatesSource;
|
||||
private FeeRatesSelection feeRatesSelection;
|
||||
private OptimizationStrategy sendOptimizationStrategy;
|
||||
@@ -44,6 +46,7 @@ public class Config {
|
||||
private boolean showTransactionHex = true;
|
||||
private boolean showLoadingLog = true;
|
||||
private boolean showAddressTransactionCount = false;
|
||||
private boolean showDeprecatedImportExport = false;
|
||||
private boolean preventSleep = false;
|
||||
private List<File> recentWalletFiles;
|
||||
private Integer keyDerivationPeriod;
|
||||
@@ -53,14 +56,14 @@ public class Config {
|
||||
private Boolean hdCapture;
|
||||
private String webcamDevice;
|
||||
private ServerType serverType;
|
||||
private String publicElectrumServer;
|
||||
private String coreServer;
|
||||
private List<String> recentCoreServers;
|
||||
private Server publicElectrumServer;
|
||||
private Server coreServer;
|
||||
private List<Server> recentCoreServers;
|
||||
private CoreAuthType coreAuthType;
|
||||
private File coreDataDir;
|
||||
private String coreAuth;
|
||||
private String electrumServer;
|
||||
private List<String> recentElectrumServers;
|
||||
private Server electrumServer;
|
||||
private List<Server> recentElectrumServers;
|
||||
private File electrumServerCert;
|
||||
private boolean useProxy;
|
||||
private String proxyServer;
|
||||
@@ -77,6 +80,8 @@ public class Config {
|
||||
GsonBuilder gsonBuilder = new GsonBuilder();
|
||||
gsonBuilder.registerTypeAdapter(File.class, new FileSerializer());
|
||||
gsonBuilder.registerTypeAdapter(File.class, new FileDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(Server.class, new ServerSerializer());
|
||||
gsonBuilder.registerTypeAdapter(Server.class, new ServerDeserializer());
|
||||
return gsonBuilder.setPrettyPrinting().disableHtmlEscaping().create();
|
||||
}
|
||||
|
||||
@@ -131,6 +136,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public UnitFormat getUnitFormat() {
|
||||
return unitFormat;
|
||||
}
|
||||
|
||||
public void setUnitFormat(UnitFormat unitFormat) {
|
||||
this.unitFormat = unitFormat;
|
||||
flush();
|
||||
}
|
||||
|
||||
public FeeRatesSource getFeeRatesSource() {
|
||||
return feeRatesSource;
|
||||
}
|
||||
@@ -288,6 +302,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isShowDeprecatedImportExport() {
|
||||
return showDeprecatedImportExport;
|
||||
}
|
||||
|
||||
public void setShowDeprecatedImportExport(boolean showDeprecatedImportExport) {
|
||||
this.showDeprecatedImportExport = showDeprecatedImportExport;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isPreventSleep() {
|
||||
return preventSleep;
|
||||
}
|
||||
@@ -362,14 +385,18 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean hasServerAddress() {
|
||||
return getServerAddress() != null && !getServerAddress().isEmpty();
|
||||
public boolean hasServer() {
|
||||
return getServer() != null;
|
||||
}
|
||||
|
||||
public String getServerAddress() {
|
||||
public Server getServer() {
|
||||
return getServerType() == ServerType.BITCOIN_CORE ? getCoreServer() : (getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? getPublicElectrumServer() : getElectrumServer());
|
||||
}
|
||||
|
||||
public String getServerDisplayName() {
|
||||
return getServer() == null ? "server" : getServer().getDisplayName();
|
||||
}
|
||||
|
||||
public boolean requiresInternalTor() {
|
||||
if(isUseProxy()) {
|
||||
return false;
|
||||
@@ -379,57 +406,71 @@ public class Config {
|
||||
}
|
||||
|
||||
public boolean requiresTor() {
|
||||
if(!hasServerAddress()) {
|
||||
if(!hasServer()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Protocol protocol = Protocol.getProtocol(getServerAddress());
|
||||
if(protocol == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return protocol.isOnionAddress(protocol.getServerHostAndPort(getServerAddress()));
|
||||
return getServer().isOnionAddress();
|
||||
}
|
||||
|
||||
public String getPublicElectrumServer() {
|
||||
public Server getPublicElectrumServer() {
|
||||
return publicElectrumServer;
|
||||
}
|
||||
|
||||
public void setPublicElectrumServer(String publicElectrumServer) {
|
||||
public void setPublicElectrumServer(Server publicElectrumServer) {
|
||||
this.publicElectrumServer = publicElectrumServer;
|
||||
flush();
|
||||
}
|
||||
|
||||
public void changePublicServer() {
|
||||
List<String> otherServers = PublicElectrumServer.getServers().stream().map(PublicElectrumServer::getUrl).filter(url -> !url.equals(getPublicElectrumServer())).collect(Collectors.toList());
|
||||
List<Server> otherServers = PublicElectrumServer.getServers().stream().map(PublicElectrumServer::getServer).filter(server -> !server.equals(getPublicElectrumServer())).collect(Collectors.toList());
|
||||
if(!otherServers.isEmpty()) {
|
||||
setPublicElectrumServer(otherServers.get(new Random().nextInt(otherServers.size())));
|
||||
}
|
||||
}
|
||||
|
||||
public String getCoreServer() {
|
||||
public Server getCoreServer() {
|
||||
return coreServer;
|
||||
}
|
||||
|
||||
public void setCoreServer(String coreServer) {
|
||||
public void setCoreServer(Server coreServer) {
|
||||
this.coreServer = coreServer;
|
||||
flush();
|
||||
}
|
||||
|
||||
public List<String> getRecentCoreServers() {
|
||||
return recentCoreServers;
|
||||
public List<Server> getRecentCoreServers() {
|
||||
return recentCoreServers == null ? new ArrayList<>() : recentCoreServers;
|
||||
}
|
||||
|
||||
public void addRecentCoreServer(String coreServer) {
|
||||
public boolean addRecentCoreServer(Server coreServer) {
|
||||
if(recentCoreServers == null) {
|
||||
recentCoreServers = new ArrayList<>();
|
||||
}
|
||||
|
||||
if(!recentCoreServers.contains(coreServer)) {
|
||||
recentCoreServers.stream().filter(url -> Objects.equals(Protocol.getHost(url), Protocol.getHost(coreServer)))
|
||||
.findFirst().ifPresent(existingUrl -> recentCoreServers.remove(existingUrl));
|
||||
int index = getRecentCoreServers().indexOf(coreServer);
|
||||
if(index < 0) {
|
||||
recentCoreServers.removeIf(server -> server.getHost().equals(coreServer.getHost()) && server.getAlias() == null);
|
||||
recentCoreServers.add(coreServer);
|
||||
flush();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void removeRecentCoreServer(Server server) {
|
||||
int index = getRecentCoreServers().indexOf(server);
|
||||
if(index >= 0) {
|
||||
recentCoreServers.remove(index);
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
public void setCoreServerAlias(Server server) {
|
||||
int index = getRecentCoreServers().indexOf(server);
|
||||
if(index >= 0) {
|
||||
recentCoreServers.set(index, server);
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,37 +501,59 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public String getElectrumServer() {
|
||||
public Server getElectrumServer() {
|
||||
return electrumServer;
|
||||
}
|
||||
|
||||
public void setElectrumServer(String electrumServer) {
|
||||
public void setElectrumServer(Server electrumServer) {
|
||||
this.electrumServer = electrumServer;
|
||||
flush();
|
||||
}
|
||||
|
||||
public List<String> getRecentElectrumServers() {
|
||||
return recentElectrumServers;
|
||||
public List<Server> getRecentElectrumServers() {
|
||||
return recentElectrumServers == null ? new ArrayList<>() : recentElectrumServers;
|
||||
}
|
||||
|
||||
public void addRecentServer() {
|
||||
public boolean addRecentServer() {
|
||||
if(serverType == ServerType.BITCOIN_CORE && coreServer != null) {
|
||||
addRecentCoreServer(coreServer);
|
||||
return addRecentCoreServer(coreServer);
|
||||
} else if(serverType == ServerType.ELECTRUM_SERVER && electrumServer != null) {
|
||||
addRecentElectrumServer(electrumServer);
|
||||
return addRecentElectrumServer(electrumServer);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void addRecentElectrumServer(String electrumServer) {
|
||||
public boolean addRecentElectrumServer(Server electrumServer) {
|
||||
if(recentElectrumServers == null) {
|
||||
recentElectrumServers = new ArrayList<>();
|
||||
}
|
||||
|
||||
if(!recentElectrumServers.contains(electrumServer)) {
|
||||
recentElectrumServers.stream().filter(url -> Objects.equals(Protocol.getHost(url), Protocol.getHost(electrumServer)))
|
||||
.findFirst().ifPresent(existingUrl -> recentElectrumServers.remove(existingUrl));
|
||||
int index = getRecentElectrumServers().indexOf(electrumServer);
|
||||
if(index < 0) {
|
||||
recentElectrumServers.removeIf(server -> server.getHost().equals(electrumServer.getHost()) && server.getAlias() == null);
|
||||
recentElectrumServers.add(electrumServer);
|
||||
flush();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void removeRecentElectrumServer(Server server) {
|
||||
int index = getRecentElectrumServers().indexOf(server);
|
||||
if(index >= 0) {
|
||||
recentElectrumServers.remove(index);
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
public void setElectrumServerAlias(Server server) {
|
||||
int index = getRecentElectrumServers().indexOf(server);
|
||||
if(index >= 0) {
|
||||
recentElectrumServers.set(index, server);
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,4 +658,18 @@ public class Config {
|
||||
return new File(json.getAsJsonPrimitive().getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServerSerializer implements JsonSerializer<Server> {
|
||||
@Override
|
||||
public JsonElement serialize(Server src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServerDeserializer implements JsonDeserializer<Server> {
|
||||
@Override
|
||||
public Server deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return Server.fromString(json.getAsJsonPrimitive().getAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,18 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
String outputDescriptor = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n"));
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
inputStream.transferTo(baos);
|
||||
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray());
|
||||
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray());
|
||||
|
||||
try {
|
||||
return PdfUtils.getOutputDescriptor(firstClone).toWallet();
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
String outputDescriptor = new BufferedReader(new InputStreamReader(secondClone, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n"));
|
||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(outputDescriptor.trim());
|
||||
return descriptor.toWallet();
|
||||
} catch(Exception e) {
|
||||
|
||||
@@ -193,6 +193,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
keystore.setLabel(keystore.getLabel().substring(0, Keystore.MAX_LABEL_LENGTH));
|
||||
}
|
||||
|
||||
wallet.makeLabelsUnique(keystore);
|
||||
wallet.getKeystores().add(keystore);
|
||||
|
||||
ExtendedKey.Header xpubHeader = ExtendedKey.Header.fromExtendedKey(ek.xpub);
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class ElectrumPersonalServer implements WalletExport {
|
||||
private static final Logger log = LoggerFactory.getLogger(ElectrumPersonalServer.class);
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Electrum Personal Server";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.EPS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
if(wallet.getScriptType() == ScriptType.P2TR) {
|
||||
throw new ExportException(getName() + " does not support Taproot wallets.");
|
||||
}
|
||||
|
||||
try {
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
writer.write("# Electrum Personal Server configuration file fragments\n");
|
||||
writer.write("# Copy the lines below into the relevant sections in your EPS config.ini file\n\n");
|
||||
writer.write("# Copy into [master-public-keys] section\n");
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
writeWalletXpub(masterWallet, writer);
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
writeWalletXpub(childWallet, writer);
|
||||
}
|
||||
}
|
||||
|
||||
writeBip47Addresses(masterWallet, writer);
|
||||
|
||||
writer.flush();
|
||||
} catch(Exception e) {
|
||||
log.error("Could not export EPS wallet", e);
|
||||
throw new ExportException("Could not export EPS wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeWalletXpub(Wallet wallet, BufferedWriter writer) throws IOException {
|
||||
writer.write(wallet.getFullName().replace(' ', '_') + " = ");
|
||||
|
||||
ExtendedKey.Header xpubHeader = ExtendedKey.Header.fromScriptType(wallet.getScriptType(), false);
|
||||
if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
writer.write(wallet.getDefaultPolicy().getNumSignaturesRequired() + " ");
|
||||
}
|
||||
|
||||
for(Iterator<Keystore> iter = wallet.getKeystores().iterator(); iter.hasNext(); ) {
|
||||
Keystore keystore = iter.next();
|
||||
writer.write(keystore.getExtendedPublicKey().toString(xpubHeader));
|
||||
|
||||
if(iter.hasNext()) {
|
||||
writer.write(" ");
|
||||
}
|
||||
}
|
||||
|
||||
writer.newLine();
|
||||
}
|
||||
|
||||
private static void writeBip47Addresses(Wallet wallet, BufferedWriter writer) throws IOException {
|
||||
if(wallet.hasPaymentCode()) {
|
||||
writer.write("\n# Copy into [watch-only-addresses] section\n");
|
||||
WalletNode notificationNode = wallet.getNotificationWallet().getNode(KeyPurpose.NOTIFICATION);
|
||||
writer.write(wallet.getFullName().replace(' ', '_') + "-notification_addr = " + notificationNode.getAddress().toString() + "\n");
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isBip47()) {
|
||||
writer.write(childWallet.getFullName().replace(' ', '_') + " = ");
|
||||
for(Iterator<KeyPurpose> purposeIterator = KeyPurpose.DEFAULT_PURPOSES.iterator(); purposeIterator.hasNext(); ) {
|
||||
KeyPurpose keyPurpose = purposeIterator.next();
|
||||
for(Iterator<WalletNode> iter = childWallet.getNode(keyPurpose).getChildren().iterator(); iter.hasNext(); ) {
|
||||
WalletNode receiveNode = iter.next();
|
||||
writer.write(receiveNode.getAddress().toString());
|
||||
|
||||
if(iter.hasNext()) {
|
||||
writer.write(" ");
|
||||
}
|
||||
}
|
||||
|
||||
if(purposeIterator.hasNext()) {
|
||||
writer.write(" ");
|
||||
}
|
||||
}
|
||||
|
||||
writer.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
writer.write("\n# Important: If this wallet receives any BIP47 payments, redo this export");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "Export this wallet as a configuration file fragment to copy into your Electrum Personal Server (EPS) config.ini file.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
return "ini";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean walletExportRequiresDecryption() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exportsAllWallets() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,6 @@ package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public interface FileImport extends Import {
|
||||
public interface FileImport extends ImportExport {
|
||||
boolean isEncrypted(File file);
|
||||
}
|
||||
|
||||
@@ -300,6 +300,7 @@ public class Hwi {
|
||||
if(hwiExecutable == null || !hwiExecutable.exists()) {
|
||||
try {
|
||||
Platform platform = Platform.getCurrent();
|
||||
String osArch = System.getProperty("os.arch");
|
||||
Set<PosixFilePermission> ownerExecutableWritable = PosixFilePermissions.fromString("rwxr--r--");
|
||||
|
||||
//A PyInstaller --onefile expands into a new directory on every run triggering OSX Gatekeeper checks.
|
||||
@@ -307,7 +308,6 @@ public class Hwi {
|
||||
//The check will still happen on first invocation, but will not thereafter
|
||||
//See https://github.com/bitcoin-core/HWI/issues/327 for details
|
||||
if(platform == Platform.OSX) {
|
||||
String osArch = System.getProperty("os.arch");
|
||||
InputStream inputStream;
|
||||
if(osArch.equals("aarch64")) {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/osx/aarch64/" + HWI_VERSION_DIR + "-mac-aarch64-signed.zip");
|
||||
@@ -359,6 +359,9 @@ public class Hwi {
|
||||
Files.createDirectories(getHwiHomeDir().toPath());
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
|
||||
tempExecPath = Files.createTempFile(getHwiHomeDir().toPath(), HWI_VERSION_DIR, null);
|
||||
} else if(osArch.equals("aarch64")) {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/linux/aarch64/hwi");
|
||||
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
} else {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
|
||||
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
public interface Import {
|
||||
String getName();
|
||||
WalletModel getWalletModel();
|
||||
}
|
||||
+4
-1
@@ -2,7 +2,10 @@ package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
public interface Export {
|
||||
public interface ImportExport {
|
||||
String getName();
|
||||
WalletModel getWalletModel();
|
||||
default boolean isDeprecated() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class Jade implements KeystoreFileImport {
|
||||
@Override
|
||||
public boolean isEncrypted(File file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Jade";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.JADE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
throw new ImportException("Failed to detect a valid " + scriptType.getDescription() + " keystore.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import QR created on your Jade by selecting Xpub Export from the Settings menu once you have loaded your seed.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFileFormatAvailable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
public interface KeystoreImport extends Import {
|
||||
public interface KeystoreImport extends ImportExport {
|
||||
String getKeystoreImportDescription();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.WriterException;
|
||||
import com.google.zxing.client.j2se.MatrixToImageConfig;
|
||||
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
import com.lowagie.text.*;
|
||||
import com.lowagie.text.Font;
|
||||
import com.lowagie.text.Image;
|
||||
import com.lowagie.text.pdf.PdfReader;
|
||||
import com.lowagie.text.pdf.PdfWriter;
|
||||
import com.lowagie.text.pdf.parser.PdfTextExtractor;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.UREncoder;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.*;
|
||||
import java.util.Scanner;
|
||||
|
||||
public class PdfUtils {
|
||||
private static final Logger log = LoggerFactory.getLogger(PdfUtils.class);
|
||||
|
||||
private static final int QR_WIDTH = 480;
|
||||
private static final int QR_HEIGHT = 480;
|
||||
|
||||
public static void saveOutputDescriptor(String walletName, String outputDescriptor, UR ur) {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save PDF");
|
||||
fileChooser.setInitialFileName(walletName + ".pdf");
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
try(Document document = new Document()) {
|
||||
document.setMargins(36, 36, 48, 36);
|
||||
PdfWriter.getInstance(document, new FileOutputStream(file));
|
||||
document.open();
|
||||
|
||||
Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16, Color.BLACK);
|
||||
Chunk title = new Chunk("Output descriptor for " + walletName, titleFont);
|
||||
document.add(title);
|
||||
|
||||
UREncoder urEncoder = new UREncoder(ur, 2000, 10, 0);
|
||||
String fragment = urEncoder.nextPart();
|
||||
if(urEncoder.isSinglePart()) {
|
||||
Image image = Image.getInstance(SwingFXUtils.fromFXImage(getQrCode(fragment), null), Color.WHITE);
|
||||
document.add(image);
|
||||
}
|
||||
|
||||
Font descriptorFont = FontFactory.getFont(FontFactory.COURIER, 14, Color.BLACK);
|
||||
Paragraph descriptor = new Paragraph(outputDescriptor, descriptorFont);
|
||||
document.add(descriptor);
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating descriptor PDF", e);
|
||||
AppServices.showErrorDialog("Error creating PDF", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static OutputDescriptor getOutputDescriptor(InputStream inputStream) throws IOException {
|
||||
try {
|
||||
PdfReader pdfReader = new PdfReader(inputStream);
|
||||
String allText = "";
|
||||
for(int page = 1; page <= pdfReader.getNumberOfPages(); page++) {
|
||||
PdfTextExtractor textExtractor = new PdfTextExtractor(pdfReader);
|
||||
allText += textExtractor.getTextFromPage(page) + "\n";
|
||||
}
|
||||
|
||||
String descriptor = null;
|
||||
Scanner scanner = new Scanner(allText);
|
||||
while(scanner.hasNextLine()) {
|
||||
String line = scanner.nextLine().trim();
|
||||
if(descriptor != null) {
|
||||
descriptor += line;
|
||||
} else if(ScriptType.fromDescriptor(line) != null) {
|
||||
descriptor = line;
|
||||
}
|
||||
}
|
||||
|
||||
if(descriptor != null) {
|
||||
return OutputDescriptor.getOutputDescriptor(descriptor);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw new IllegalArgumentException("Not a valid PDF or output descriptor");
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Output descriptor could not be found");
|
||||
}
|
||||
|
||||
private static javafx.scene.image.Image getQrCode(String fragment) throws IOException, WriterException {
|
||||
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
||||
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
MatrixToImageWriter.writeToStream(qrMatrix, "PNG", baos, new MatrixToImageConfig());
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
|
||||
return new javafx.scene.image.Image(bais);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.sparrow.net.Protocol;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class Server {
|
||||
private final String url;
|
||||
private final String alias;
|
||||
|
||||
public Server(String url) {
|
||||
this(url, null);
|
||||
}
|
||||
|
||||
public Server(String url, String alias) {
|
||||
if(url == null) {
|
||||
throw new IllegalArgumentException("Url cannot be null");
|
||||
}
|
||||
|
||||
if(url.isEmpty()) {
|
||||
throw new IllegalArgumentException("Url cannot be empty");
|
||||
}
|
||||
|
||||
if(Protocol.getProtocol(url) == null) {
|
||||
throw new IllegalArgumentException("Unknown protocol for url " + url + ", must be one of " + Arrays.toString(Protocol.values()));
|
||||
}
|
||||
|
||||
if(Protocol.getHost(url) == null) {
|
||||
throw new IllegalArgumentException("Cannot determine host for url " + url);
|
||||
}
|
||||
|
||||
if(alias != null && alias.isEmpty()) {
|
||||
throw new IllegalArgumentException("Server alias cannot be an empty string");
|
||||
}
|
||||
|
||||
this.url = url;
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public Protocol getProtocol() {
|
||||
return Protocol.getProtocol(url);
|
||||
}
|
||||
|
||||
public HostAndPort getHostAndPort() {
|
||||
return getProtocol().getServerHostAndPort(url);
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return getHostAndPort().getHost();
|
||||
}
|
||||
|
||||
public boolean isOnionAddress() {
|
||||
return Protocol.isOnionAddress(getHostAndPort());
|
||||
}
|
||||
|
||||
public String getAlias() {
|
||||
return alias;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return alias == null ? url : alias;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return url + (alias == null ? "" : "|" + alias);
|
||||
}
|
||||
|
||||
public static Server fromString(String server) {
|
||||
String[] parts = server.split("\\|");
|
||||
if(parts.length >= 2) {
|
||||
return new Server(parts[0], parts[1]);
|
||||
}
|
||||
|
||||
return new Server(parts[0], null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
}
|
||||
if(o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Server server = (Server)o;
|
||||
return url.equals(server.url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return url.hashCode();
|
||||
}
|
||||
}
|
||||
@@ -144,4 +144,9 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
public boolean isWalletImportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exportsAllWallets() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.MainApp;
|
||||
import com.sparrowwallet.sparrow.SparrowWallet;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
||||
@@ -136,6 +139,98 @@ public class Storage {
|
||||
closePersistenceService.start();
|
||||
}
|
||||
|
||||
public void restorePublicKeysFromSeed(Wallet wallet, Key key) throws MnemonicException {
|
||||
checkWalletNetwork(wallet);
|
||||
|
||||
if(wallet.containsMasterPrivateKeys()) {
|
||||
//Derive xpub and master fingerprint from seed, potentially with passphrase
|
||||
Wallet copy = wallet.copy();
|
||||
for(int i = 0; i < copy.getKeystores().size(); i++) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
if(copyKeystore.hasSeed() && copyKeystore.getSeed().getPassphrase() == null) {
|
||||
if(copyKeystore.getSeed().needsPassphrase()) {
|
||||
if(!wallet.isMasterWallet() && wallet.getMasterWallet().getKeystores().size() == copy.getKeystores().size() && wallet.getMasterWallet().getKeystores().get(i).hasSeed()) {
|
||||
copyKeystore.getSeed().setPassphrase(wallet.getMasterWallet().getKeystores().get(i).getSeed().getPassphrase());
|
||||
} else {
|
||||
Optional<String> optionalPassphrase = AppServices.getInteractionServices().requestPassphrase(wallet.getFullDisplayName(), copyKeystore);
|
||||
if(optionalPassphrase.isPresent()) {
|
||||
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
copyKeystore.getSeed().setPassphrase("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(wallet.isEncrypted()) {
|
||||
if(key == null) {
|
||||
throw new IllegalStateException("Wallet was not encrypted, but seed is");
|
||||
}
|
||||
|
||||
copy.decrypt(key);
|
||||
}
|
||||
|
||||
if(wallet.isWhirlpoolMasterWallet()) {
|
||||
String walletId = getWalletId(wallet);
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
|
||||
whirlpool.setHDWallet(getWalletId(wallet), copy);
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
soroban.setHDWallet(copy);
|
||||
}
|
||||
|
||||
StandardAccount standardAccount = wallet.getStandardAccountType();
|
||||
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
|
||||
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
|
||||
}
|
||||
|
||||
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
||||
Keystore keystore = wallet.getKeystores().get(i);
|
||||
if(keystore.hasSeed()) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
Keystore derivedKeystore = Keystore.fromSeed(copyKeystore.getSeed(), copyKeystore.getKeyDerivation().getDerivation());
|
||||
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
||||
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
||||
keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase());
|
||||
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
||||
copyKeystore.getSeed().clear();
|
||||
} else if(keystore.hasMasterPrivateExtendedKey()) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), copyKeystore.getKeyDerivation().getDerivation());
|
||||
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
||||
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
||||
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
||||
copyKeystore.getMasterPrivateKey().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isBip47()) {
|
||||
try {
|
||||
Keystore masterKeystore = wallet.getKeystores().get(0);
|
||||
Keystore keystore = childWallet.getKeystores().get(0);
|
||||
keystore.setBip47ExtendedPrivateKey(masterKeystore.getBip47ExtendedPrivateKey());
|
||||
List<ChildNumber> derivation = keystore.getKeyDerivation().getDerivation();
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterKeystore.getKeyDerivation().getMasterFingerprint(), derivation));
|
||||
DeterministicKey pubKey = keystore.getBip47ExtendedPrivateKey().getKey().dropPrivateBytes().dropParent();
|
||||
keystore.setExtendedPublicKey(new ExtendedKey(pubKey, keystore.getBip47ExtendedPrivateKey().getParentFingerprint(), derivation.get(derivation.size() - 1)));
|
||||
} catch(Exception e) {
|
||||
log.error("Cannot prepare BIP47 keystore", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkWalletNetwork(Wallet wallet) {
|
||||
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.get()) {
|
||||
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.get() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
|
||||
}
|
||||
}
|
||||
|
||||
public void backupWallet() throws IOException {
|
||||
if(walletFile.toPath().startsWith(getWalletsDir().toPath())) {
|
||||
backupWallet(null);
|
||||
@@ -452,8 +547,8 @@ public class Storage {
|
||||
}
|
||||
|
||||
public static File getSparrowHome() {
|
||||
if(System.getProperty(MainApp.APP_HOME_PROPERTY) != null) {
|
||||
return new File(System.getProperty(MainApp.APP_HOME_PROPERTY));
|
||||
if(System.getProperty(SparrowWallet.APP_HOME_PROPERTY) != null) {
|
||||
return new File(System.getProperty(SparrowWallet.APP_HOME_PROPERTY));
|
||||
}
|
||||
|
||||
if(Platform.getCurrent() == Platform.WINDOWS) {
|
||||
|
||||
@@ -4,10 +4,13 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface WalletExport extends Export {
|
||||
public interface WalletExport extends ImportExport {
|
||||
void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException;
|
||||
String getWalletExportDescription();
|
||||
String getExportFileExtension(Wallet wallet);
|
||||
boolean isWalletExportScannable();
|
||||
boolean walletExportRequiresDecryption();
|
||||
default boolean exportsAllWallets() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface WalletImport extends Import, FileImport {
|
||||
public interface WalletImport extends FileImport {
|
||||
String getWalletImportDescription();
|
||||
Wallet importWallet(InputStream inputStream, String password) throws ImportException;
|
||||
boolean isWalletImportScannable();
|
||||
|
||||
@@ -18,15 +18,17 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
public void initializeView() {
|
||||
List<KeystoreFileImport> importers = Collections.emptyList();
|
||||
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
|
||||
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
||||
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
||||
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
|
||||
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
||||
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
||||
}
|
||||
|
||||
for(KeystoreFileImport importer : importers) {
|
||||
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
|
||||
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == importer.getWalletModel()) {
|
||||
importAccordion.getPanes().add(importPane);
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
|
||||
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == importer.getWalletModel()) {
|
||||
importAccordion.getPanes().add(importPane);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ public class SwController extends KeystoreImportDetailController {
|
||||
List<KeystoreImport> importers = List.of(new Bip39(), new Electrum(), new Bip32());
|
||||
|
||||
for(KeystoreImport importer : importers) {
|
||||
if(importer.isDeprecated() && !Config.get().isShowDeprecatedImportExport()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TitledDescriptionPane importPane = null;
|
||||
|
||||
if(importer instanceof KeystoreFileImport) {
|
||||
|
||||
@@ -123,8 +123,8 @@ public class Auth47 {
|
||||
}
|
||||
|
||||
private void send(String json) throws IOException, Auth47Exception {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("Sending " + json + " to " + callback);
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Sending auth47 response " + json + " to " + callback);
|
||||
}
|
||||
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
|
||||
@@ -146,6 +146,10 @@ public enum BroadcastSource {
|
||||
try {
|
||||
URL url = getURL(proxy);
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Broadcasting transaction to " + url);
|
||||
}
|
||||
|
||||
HttpURLConnection connection = proxy == null ? (HttpURLConnection)url.openConnection() : (HttpURLConnection)url.openConnection(proxy);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "text/plain");
|
||||
|
||||
@@ -51,6 +51,8 @@ public class Bwt {
|
||||
NativeUtils.loadLibraryFromJar("/native/osx/x64/libbwt_jni.dylib");
|
||||
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
NativeUtils.loadLibraryFromJar("/native/windows/x64/bwt_jni.dll");
|
||||
} else if(osArch.equals("aarch64")) {
|
||||
NativeUtils.loadLibraryFromJar("/native/linux/aarch64/libbwt_jni.so");
|
||||
} else {
|
||||
NativeUtils.loadLibraryFromJar("/native/linux/x64/libbwt_jni.so");
|
||||
}
|
||||
@@ -143,8 +145,8 @@ public class Bwt {
|
||||
bwtConfig.electrumSkipMerkle = true;
|
||||
|
||||
Config config = Config.get();
|
||||
bwtConfig.bitcoindUrl = config.getCoreServer();
|
||||
if(bwtConfig.bitcoindUrl != null) {
|
||||
if(config.getCoreServer() != null) {
|
||||
bwtConfig.bitcoindUrl = config.getCoreServer().getUrl();
|
||||
try {
|
||||
HostAndPort hostAndPort = Protocol.HTTP.getServerHostAndPort(bwtConfig.bitcoindUrl);
|
||||
if(hostAndPort.getHost().endsWith(".local")) {
|
||||
@@ -308,7 +310,7 @@ public class Bwt {
|
||||
Bwt.this.shutdown();
|
||||
terminating = false;
|
||||
} else {
|
||||
Platform.runLater(() -> EventManager.get().post(new BwtBootStatusEvent("Connecting to Bitcoin Core node at " + Config.get().getCoreServer() + "...")));
|
||||
Platform.runLater(() -> EventManager.get().post(new BwtBootStatusEvent("Connecting to Bitcoin Core node " + Config.get().getServerDisplayName() + "...")));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.github.arteam.simplejsonrpc.client.Transport;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
@@ -15,6 +14,7 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Server;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNym;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
@@ -51,7 +51,7 @@ public class ElectrumServer {
|
||||
|
||||
private static final Map<String, List<String>> subscribedScriptHashes = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private static String previousServerAddress;
|
||||
private static Server previousServer;
|
||||
|
||||
private static Map<String, String> retrievedScriptHashes = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
@@ -59,14 +59,14 @@ public class ElectrumServer {
|
||||
|
||||
private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc();
|
||||
|
||||
private static String bwtElectrumServer;
|
||||
private static Server bwtElectrumServer;
|
||||
|
||||
private static final Pattern RPC_WALLET_LOADING_PATTERN = Pattern.compile(".*\"(Wallet loading failed:[^\"]*)\".*");
|
||||
|
||||
private static synchronized CloseableTransport getTransport() throws ServerException {
|
||||
if(transport == null) {
|
||||
try {
|
||||
String electrumServer = null;
|
||||
Server electrumServer = null;
|
||||
File electrumServerCert = null;
|
||||
String proxyServer = null;
|
||||
|
||||
@@ -78,8 +78,8 @@ public class ElectrumServer {
|
||||
throw new ServerConfigException("Could not connect to Bitcoin Core RPC");
|
||||
}
|
||||
electrumServer = bwtElectrumServer;
|
||||
if(previousServerAddress != null && previousServerAddress.contains(Bwt.ELECTRUM_HOST)) {
|
||||
previousServerAddress = bwtElectrumServer;
|
||||
if(previousServer != null && previousServer.getUrl().contains(Bwt.ELECTRUM_HOST)) {
|
||||
previousServer = bwtElectrumServer;
|
||||
}
|
||||
} else if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
|
||||
electrumServer = Config.get().getElectrumServer();
|
||||
@@ -95,33 +95,30 @@ public class ElectrumServer {
|
||||
throw new ServerConfigException("Electrum server certificate file not found");
|
||||
}
|
||||
|
||||
Protocol protocol = Protocol.getProtocol(electrumServer);
|
||||
if(protocol == null) {
|
||||
throw new ServerConfigException("Electrum server URL must start with " + Protocol.TCP.toUrlString() + " or " + Protocol.SSL.toUrlString());
|
||||
}
|
||||
Protocol protocol = electrumServer.getProtocol();
|
||||
|
||||
//If changing server, don't rely on previous transaction history
|
||||
if(previousServerAddress != null && !electrumServer.equals(previousServerAddress)) {
|
||||
if(previousServer != null && !electrumServer.equals(previousServer)) {
|
||||
retrievedScriptHashes.clear();
|
||||
retrievedTransactions.clear();
|
||||
}
|
||||
previousServerAddress = electrumServer;
|
||||
previousServer = electrumServer;
|
||||
|
||||
HostAndPort server = protocol.getServerHostAndPort(electrumServer);
|
||||
boolean localNetworkAddress = !protocol.isOnionAddress(server) && IpAddressMatcher.isLocalNetworkAddress(server.getHost());
|
||||
HostAndPort hostAndPort = electrumServer.getHostAndPort();
|
||||
boolean localNetworkAddress = !protocol.isOnionAddress(hostAndPort) && IpAddressMatcher.isLocalNetworkAddress(hostAndPort.getHost());
|
||||
|
||||
if(!localNetworkAddress && Config.get().isUseProxy() && proxyServer != null && !proxyServer.isBlank()) {
|
||||
HostAndPort proxy = HostAndPort.fromString(proxyServer);
|
||||
if(electrumServerCert != null) {
|
||||
transport = protocol.getTransport(server, electrumServerCert, proxy);
|
||||
transport = protocol.getTransport(hostAndPort, electrumServerCert, proxy);
|
||||
} else {
|
||||
transport = protocol.getTransport(server, proxy);
|
||||
transport = protocol.getTransport(hostAndPort, proxy);
|
||||
}
|
||||
} else {
|
||||
if(electrumServerCert != null) {
|
||||
transport = protocol.getTransport(server, electrumServerCert);
|
||||
transport = protocol.getTransport(hostAndPort, electrumServerCert);
|
||||
} else {
|
||||
transport = protocol.getTransport(server);
|
||||
transport = protocol.getTransport(hostAndPort);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -364,7 +361,7 @@ public class ElectrumServer {
|
||||
}
|
||||
|
||||
private int getGapLimitSize(Wallet wallet, Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap) {
|
||||
int highestIndex = nodeTransactionMap.keySet().stream().map(WalletNode::getIndex).max(Comparator.comparing(Integer::valueOf)).orElse(-1);
|
||||
int highestIndex = nodeTransactionMap.keySet().stream().filter(node -> node.getDerivation().size() > 1).map(WalletNode::getIndex).max(Comparator.comparing(Integer::valueOf)).orElse(-1);
|
||||
return highestIndex + wallet.getGapLimit() + 1;
|
||||
}
|
||||
|
||||
@@ -1251,7 +1248,7 @@ public class ElectrumServer {
|
||||
@Subscribe
|
||||
public void bwtElectrumReadyStatus(BwtElectrumReadyStatusEvent event) {
|
||||
if(this.isRunning()) {
|
||||
ElectrumServer.bwtElectrumServer = Protocol.TCP.toUrlString(HostAndPort.fromString(event.getElectrumAddr()));
|
||||
ElectrumServer.bwtElectrumServer = new Server(Protocol.TCP.toUrlString(HostAndPort.fromString(event.getElectrumAddr())));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1637,6 +1634,10 @@ public class ElectrumServer {
|
||||
}
|
||||
|
||||
private List<StandardAccount> getStandardAccounts(Wallet wallet) {
|
||||
if(!wallet.getKeystores().stream().allMatch(Keystore::hasMasterPrivateKey)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<StandardAccount> accounts = new ArrayList<>();
|
||||
for(StandardAccount account : StandardAccount.values()) {
|
||||
if(account != StandardAccount.ACCOUNT_0 && (!StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account) || wallet.getScriptType() == ScriptType.P2WPKH)) {
|
||||
|
||||
@@ -52,6 +52,10 @@ public enum ExchangeSource {
|
||||
String url = "https://api.coinbase.com/v2/exchange-rates?currency=BTC";
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting exchange rates from " + url);
|
||||
}
|
||||
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
||||
Gson gson = new Gson();
|
||||
return gson.fromJson(reader, CoinbaseRates.class);
|
||||
@@ -87,6 +91,10 @@ public enum ExchangeSource {
|
||||
String url = "https://api.coingecko.com/api/v3/exchange_rates";
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting exchange rates from " + url);
|
||||
}
|
||||
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
||||
Gson gson = new Gson();
|
||||
return gson.fromJson(reader, CoinGeckoRates.class);
|
||||
|
||||
@@ -68,6 +68,10 @@ public enum FeeRatesSource {
|
||||
private static Map<Integer, Double> getThreeTierFeeRates(Map<Integer, Double> defaultblockTargetFeeRates, String url) {
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting fee rates from " + url);
|
||||
}
|
||||
|
||||
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream()); Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
|
||||
Gson gson = new Gson();
|
||||
|
||||
@@ -97,7 +97,7 @@ public final class IpAddressMatcher {
|
||||
return InetAddress.getByName(address);
|
||||
}
|
||||
catch (UnknownHostException e) {
|
||||
throw new IllegalArgumentException("Failed to parse address" + address, e);
|
||||
throw new IllegalArgumentException("Failed to parse address: " + address, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,6 +103,10 @@ public class LnurlAuth {
|
||||
public void sendResponse(Wallet wallet) throws LnurlAuthException, IOException {
|
||||
URL callback = getReturnURL(wallet);
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Sending LNURL-auth response to " + callback);
|
||||
}
|
||||
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
if(proxy == null && callback.getHost().toLowerCase(Locale.ROOT).endsWith(TorService.TOR_ADDRESS_SUFFIX)) {
|
||||
throw new LnurlAuthException("A Tor proxy must be configured to authenticate this resource.");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.github.arteam.simplejsonrpc.client.Transport;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.sparrow.io.Server;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -117,8 +117,20 @@ public enum Protocol {
|
||||
return toUrlString() + hostAndPort.toString();
|
||||
}
|
||||
|
||||
public static boolean isOnionHost(String host) {
|
||||
return host != null && host.toLowerCase(Locale.ROOT).endsWith(TorService.TOR_ADDRESS_SUFFIX);
|
||||
}
|
||||
|
||||
public static boolean isOnionAddress(Server server) {
|
||||
if(server != null) {
|
||||
return isOnionAddress(server.getHostAndPort());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isOnionAddress(HostAndPort server) {
|
||||
return server.getHost().toLowerCase(Locale.ROOT).endsWith(TorService.TOR_ADDRESS_SUFFIX);
|
||||
return isOnionHost(server.getHost());
|
||||
}
|
||||
|
||||
public static boolean isOnionAddress(String address) {
|
||||
|
||||
@@ -33,12 +33,12 @@ public class ProxySocketFactory extends SocketFactory {
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String address, int port) throws IOException {
|
||||
return createSocket(new InetSocketAddress(address, port), null);
|
||||
return createSocket(createAddress(address, port), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String address, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||
return createSocket(new InetSocketAddress(address, port), new InetSocketAddress(localAddress, localPort));
|
||||
return createSocket(createAddress(address, port), new InetSocketAddress(localAddress, localPort));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,6 +51,14 @@ public class ProxySocketFactory extends SocketFactory {
|
||||
return createSocket(new InetSocketAddress(address, port), new InetSocketAddress(localAddress, localPort));
|
||||
}
|
||||
|
||||
private InetSocketAddress createAddress(String address, int port) {
|
||||
if(Protocol.isOnionHost(address)) {
|
||||
return InetSocketAddress.createUnresolved(address, port);
|
||||
} else {
|
||||
return new InetSocketAddress(address, port);
|
||||
}
|
||||
}
|
||||
|
||||
private Socket createSocket(InetSocketAddress address, InetSocketAddress bindAddress) throws IOException {
|
||||
Socket socket = new Socket(proxy);
|
||||
if(bindAddress != null) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.sparrow.io.Server;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -15,23 +16,21 @@ public enum PublicElectrumServer {
|
||||
TESTNET_ARANGUREN_ORG("testnet.aranguren.org", "ssl://testnet.aranguren.org:51002", Network.TESTNET);
|
||||
|
||||
PublicElectrumServer(String name, String url, Network network) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.server = new Server(url, name);
|
||||
this.network = network;
|
||||
}
|
||||
|
||||
public static final List<Network> SUPPORTED_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
|
||||
private final String name;
|
||||
private final String url;
|
||||
private final Server server;
|
||||
private final Network network;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
public Server getServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
return server.getUrl();
|
||||
}
|
||||
|
||||
public Network getNetwork() {
|
||||
@@ -46,10 +45,10 @@ public enum PublicElectrumServer {
|
||||
return SUPPORTED_NETWORKS.contains(Network.get());
|
||||
}
|
||||
|
||||
public static PublicElectrumServer fromUrl(String url) {
|
||||
for(PublicElectrumServer server : values()) {
|
||||
if(server.url.equals(url)) {
|
||||
return server;
|
||||
public static PublicElectrumServer fromServer(Server server) {
|
||||
for(PublicElectrumServer publicServer : values()) {
|
||||
if(publicServer.getServer().equals(server)) {
|
||||
return publicServer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +57,6 @@ public enum PublicElectrumServer {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
return server.getAlias();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +111,8 @@ public class TcpOverTlsTransport extends TcpTransport {
|
||||
|
||||
protected boolean shouldSaveCertificate() {
|
||||
//Avoid saving the certificates for blockstream.info public servers - they change too often and encourage approval complacency
|
||||
if(PublicElectrumServer.BLOCKSTREAM_INFO.getName().equals(server.getHost()) || PublicElectrumServer.ELECTRUM_BLOCKSTREAM_INFO.getName().equals(server.getHost())) {
|
||||
if(PublicElectrumServer.BLOCKSTREAM_INFO.getServer().getHost().equals(server.getHost())
|
||||
|| PublicElectrumServer.ELECTRUM_BLOCKSTREAM_INFO.getServer().getHost().equals(server.getHost())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,14 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
||||
}
|
||||
|
||||
private void writeRequest(String request) throws IOException {
|
||||
if(log.isTraceEnabled()) {
|
||||
log.trace("Sending to electrum server at " + server + ": " + request);
|
||||
}
|
||||
|
||||
if(socket == null) {
|
||||
throw new IllegalStateException("Socket connection has not been established.");
|
||||
}
|
||||
|
||||
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())));
|
||||
out.println(request);
|
||||
out.flush();
|
||||
@@ -203,7 +211,7 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
||||
String response = readLine(in);
|
||||
|
||||
if(response == null) {
|
||||
throw new IOException("Could not connect to server at " + Config.get().getServerAddress());
|
||||
throw new IOException("Could not connect to server" + (Config.get().hasServer() ? " at " + Config.get().getServer().getUrl() : ""));
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.MainApp;
|
||||
import com.sparrowwallet.sparrow.SparrowWallet;
|
||||
import com.sparrowwallet.sparrow.event.VersionUpdatedEvent;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
@@ -47,6 +47,11 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
|
||||
private VersionCheck getVersionCheck() throws IOException {
|
||||
URL url = new URL(VERSION_CHECK_URL);
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Requesting application version check from " + url);
|
||||
}
|
||||
|
||||
HttpsURLConnection conn = (HttpsURLConnection)(proxy == null ? url.openConnection() : url.openConnection(proxy));
|
||||
|
||||
try(InputStreamReader reader = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
@@ -86,10 +91,10 @@ public class VersionCheckService extends ScheduledService<VersionUpdatedEvent> {
|
||||
private boolean isNewer(VersionCheck versionCheck) {
|
||||
try {
|
||||
Version versionCheckVersion = new Version(versionCheck.version);
|
||||
Version currentVersion = new Version(MainApp.APP_VERSION);
|
||||
Version currentVersion = new Version(SparrowWallet.APP_VERSION);
|
||||
return versionCheckVersion.compareTo(currentVersion) > 0;
|
||||
} catch(IllegalArgumentException e) {
|
||||
log.error("Invalid versions to compare: " + versionCheck.version + " to " + MainApp.APP_VERSION, e);
|
||||
log.error("Invalid versions to compare: " + versionCheck.version + " to " + SparrowWallet.APP_VERSION, e);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -15,11 +15,7 @@ import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.event.FeeRatesUpdatedEvent;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.net.ServerException;
|
||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
||||
import com.sparrowwallet.sparrow.net.Protocol;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
@@ -84,6 +80,10 @@ public class Payjoin {
|
||||
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
|
||||
if(proxy == null && Protocol.isOnionHost(finalUri.getHost())) {
|
||||
throw new PayjoinReceiverException("Configure a Tor proxy to get a payjoin transaction from " + finalUri.getHost() + ".");
|
||||
}
|
||||
|
||||
HttpURLConnection connection = proxy == null ? (HttpURLConnection)finalUri.toURL().openConnection() : (HttpURLConnection)finalUri.toURL().openConnection(proxy);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "text/plain");
|
||||
@@ -117,6 +117,9 @@ public class Payjoin {
|
||||
} catch(URISyntaxException e) {
|
||||
log.error("Invalid payjoin receiver URI", e);
|
||||
throw new PayjoinReceiverException("Invalid payjoin receiver URI", e);
|
||||
} catch(FileNotFoundException e) {
|
||||
log.error("Could not find resource at payjoin URL " + uri, e);
|
||||
throw new PayjoinReceiverException("Could not find resource at payjoin URL " + uri, e);
|
||||
} catch(IOException e) {
|
||||
log.error("Payjoin receiver error", e);
|
||||
throw new PayjoinReceiverException("Payjoin receiver error", e);
|
||||
|
||||
@@ -229,7 +229,7 @@ public class PayNymController {
|
||||
followingList.setItems(FXCollections.observableList(new ArrayList<>()));
|
||||
findPayNym.setVisible(true);
|
||||
|
||||
AppServices.getPayNymService().getPayNym(nymIdentifier).subscribe(searchedPayNym -> {
|
||||
AppServices.getPayNymService().getPayNym(nymIdentifier, true).subscribe(searchedPayNym -> {
|
||||
findPayNym.setVisible(false);
|
||||
List<PayNym> searchList = new ArrayList<>();
|
||||
searchList.add(searchedPayNym);
|
||||
|
||||
@@ -51,8 +51,13 @@ public class PayNymService {
|
||||
HashMap<String, Object> body = new HashMap<>();
|
||||
body.put("code", paymentCode.toString());
|
||||
|
||||
String url = getHostUrl() + "/api/v1/create";
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Creating PayNym using " + url);
|
||||
}
|
||||
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
return httpClient.postJson(getHostUrl() + "/api/v1/create", Map.class, headers, body)
|
||||
return httpClient.postJson(url, Map.class, headers, body)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.map(Optional::get);
|
||||
@@ -69,8 +74,13 @@ public class PayNymService {
|
||||
HashMap<String, Object> body = new HashMap<>();
|
||||
body.put("code", paymentCode.toString());
|
||||
|
||||
String url = getHostUrl() + "/api/v1/token";
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Updating PayNym token using " + url);
|
||||
}
|
||||
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
return httpClient.postJson(getHostUrl() + "/api/v1/token", Map.class, headers, body)
|
||||
return httpClient.postJson(url, Map.class, headers, body)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.map(Optional::get);
|
||||
@@ -114,8 +124,13 @@ public class PayNymService {
|
||||
HashMap<String, Object> body = new HashMap<>();
|
||||
body.put("signature", signature);
|
||||
|
||||
String url = getHostUrl() + "/api/v1/claim";
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Claiming PayNym using " + url);
|
||||
}
|
||||
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
return httpClient.postJson(getHostUrl() + "/api/v1/claim", Map.class, headers, body)
|
||||
return httpClient.postJson(url, Map.class, headers, body)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.map(Optional::get);
|
||||
@@ -139,8 +154,13 @@ public class PayNymService {
|
||||
body.put("code", strPaymentCode);
|
||||
body.put("signature", signature);
|
||||
|
||||
String url = getHostUrl() + "/api/v1/nym/add";
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Adding payment code to PayNym using " + url);
|
||||
}
|
||||
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
return httpClient.postJson(getHostUrl() + "/api/v1/nym/add", Map.class, headers, body)
|
||||
return httpClient.postJson(url, Map.class, headers, body)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.map(Optional::get);
|
||||
@@ -159,32 +179,50 @@ public class PayNymService {
|
||||
body.put("signature", signature);
|
||||
body.put("target", paymentCode.toString());
|
||||
|
||||
String url = getHostUrl() + "/api/v1/follow";
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Following payment code using " + url);
|
||||
}
|
||||
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
return httpClient.postJson(getHostUrl() + "/api/v1/follow", Map.class, headers, body)
|
||||
return httpClient.postJson(url, Map.class, headers, body)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.map(Optional::get);
|
||||
}
|
||||
|
||||
public Observable<Map<String, Object>> fetchPayNym(String nymIdentifier) {
|
||||
public Observable<Map<String, Object>> fetchPayNym(String nymIdentifier, boolean compact) {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("content-type", "application/json");
|
||||
|
||||
HashMap<String, Object> body = new HashMap<>();
|
||||
body.put("nym", nymIdentifier);
|
||||
|
||||
String url = getHostUrl() + "/api/v1/nym" + (compact ? "?compact=true" : "");
|
||||
if(log.isInfoEnabled()) {
|
||||
log.info("Fetching PayNym using " + url);
|
||||
}
|
||||
|
||||
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
|
||||
return httpClient.postJson(getHostUrl() + "/api/v1/nym", Map.class, headers, body)
|
||||
return httpClient.postJson(url, Map.class, headers, body)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.map(Optional::get);
|
||||
}
|
||||
|
||||
public Observable<PayNym> getPayNym(String nymIdentifier) {
|
||||
return fetchPayNym(nymIdentifier).map(nymMap -> {
|
||||
return getPayNym(nymIdentifier, false);
|
||||
}
|
||||
|
||||
public Observable<PayNym> getPayNym(String nymIdentifier, boolean compact) {
|
||||
return fetchPayNym(nymIdentifier, compact).map(nymMap -> {
|
||||
List<Map<String, Object>> codes = (List<Map<String, Object>>)nymMap.get("codes");
|
||||
PaymentCode code = new PaymentCode((String)codes.stream().filter(codeMap -> codeMap.get("segwit") == Boolean.FALSE).map(codeMap -> codeMap.get("code")).findFirst().orElse(codes.get(0).get("code")));
|
||||
|
||||
if(compact) {
|
||||
return new PayNym(code, (String)nymMap.get("nymID"), (String)nymMap.get("nymName"), (Boolean)nymMap.get("segwit"), Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
List<Map<String, Object>> followingMaps = (List<Map<String, Object>>)nymMap.get("following");
|
||||
List<PayNym> following = followingMaps.stream().map(followingMap -> {
|
||||
return PayNym.fromString((String)followingMap.get("code"), (String)followingMap.get("nymId"), (String)followingMap.get("nymName"), (Boolean)followingMap.get("segwit"), Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
package com.sparrowwallet.sparrow.preferences;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Server;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.Event;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTableCell;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.converter.DefaultStringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ServerAliasDialog extends Dialog<Server> {
|
||||
private final ServerType serverType;
|
||||
private final TableView<ServerEntry> serverTable;
|
||||
|
||||
public ServerAliasDialog(ServerType serverType) {
|
||||
this.serverType = serverType;
|
||||
|
||||
final DialogPane dialogPane = new ServerAliasDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle("Server Aliases");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.setHeaderText("Configure aliases for recently connected servers.\nNew servers are added to this list on successful connections.");
|
||||
|
||||
Image image = new Image("/image/sparrow-small.png");
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
|
||||
serverTable = new TableView<>();
|
||||
serverTable.setPlaceholder(new Label("No servers added yet"));
|
||||
|
||||
TableColumn<ServerEntry, String> urlColumn = new TableColumn<>("URL");
|
||||
urlColumn.setMinWidth(300);
|
||||
urlColumn.setCellValueFactory((TableColumn.CellDataFeatures<ServerEntry, String> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getUrl());
|
||||
});
|
||||
|
||||
TableColumn<ServerEntry, String> aliasColumn = new TableColumn<>("Alias");
|
||||
aliasColumn.setCellValueFactory((TableColumn.CellDataFeatures<ServerEntry, String> param) -> {
|
||||
return param.getValue().labelProperty();
|
||||
});
|
||||
aliasColumn.setCellFactory(value -> new AliasCell());
|
||||
aliasColumn.setGraphic(getTagGlyph());
|
||||
|
||||
serverTable.getColumns().add(urlColumn);
|
||||
serverTable.getColumns().add(aliasColumn);
|
||||
serverTable.setEditable(true);
|
||||
serverTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||
|
||||
List<Server> servers = serverType == ServerType.BITCOIN_CORE ? Config.get().getRecentCoreServers() : Config.get().getRecentElectrumServers();
|
||||
List<ServerEntry> serverEntries = servers.stream().map(server -> new ServerEntry(serverType, server)).collect(Collectors.toList());
|
||||
serverTable.setItems(FXCollections.observableArrayList(serverEntries));
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(serverTable);
|
||||
dialogPane.setContent(stackPane);
|
||||
|
||||
ButtonType selectButtonType = new ButtonType("Select", ButtonBar.ButtonData.APPLY);
|
||||
ButtonType deleteButtonType = new ButtonType("Delete", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().addAll(deleteButtonType, ButtonType.CLOSE, selectButtonType);
|
||||
|
||||
Button selectButton = (Button)dialogPane.lookupButton(selectButtonType);
|
||||
Button deleteButton = (Button)dialogPane.lookupButton(deleteButtonType);
|
||||
selectButton.setDefaultButton(true);
|
||||
selectButton.setDisable(true);
|
||||
deleteButton.setDisable(true);
|
||||
serverTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
|
||||
selectButton.setDisable(newValue == null);
|
||||
deleteButton.setDisable(newValue == null);
|
||||
});
|
||||
|
||||
setResultConverter(buttonType -> buttonType == selectButtonType ? serverTable.getSelectionModel().getSelectedItem().getServer() : null);
|
||||
|
||||
dialogPane.setPrefWidth(680);
|
||||
dialogPane.setPrefHeight(500);
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
private static Glyph getTagGlyph() {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TAG);
|
||||
glyph.setFontSize(12);
|
||||
return glyph;
|
||||
}
|
||||
|
||||
private class ServerAliasDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
Node button;
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
Button deleteButton = new Button(buttonType.getText());
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(deleteButton, buttonData);
|
||||
deleteButton.setOnAction(event -> {
|
||||
ServerEntry serverEntry = serverTable.getSelectionModel().getSelectedItem();
|
||||
if(serverEntry != null) {
|
||||
serverTable.getItems().remove(serverEntry);
|
||||
if(serverType == ServerType.BITCOIN_CORE) {
|
||||
Config.get().removeRecentCoreServer(serverEntry.getServer());
|
||||
} else {
|
||||
Config.get().removeRecentElectrumServer(serverEntry.getServer());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
button = deleteButton;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServerEntry {
|
||||
private final Server server;
|
||||
private final StringProperty labelProperty = new SimpleStringProperty();
|
||||
|
||||
public ServerEntry(ServerType serverType, Server server) {
|
||||
this.server = server;
|
||||
labelProperty.set(server.getAlias());
|
||||
labelProperty.addListener((observable, oldValue, newValue) -> {
|
||||
String alias = newValue;
|
||||
if(alias != null && alias.isEmpty()) {
|
||||
alias = null;
|
||||
}
|
||||
|
||||
Server newServer = new Server(server.getUrl(), alias);
|
||||
if(serverType == ServerType.BITCOIN_CORE) {
|
||||
Config.get().setCoreServerAlias(newServer);
|
||||
} else {
|
||||
Config.get().setElectrumServerAlias(newServer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return server.getHostAndPort().toString();
|
||||
}
|
||||
|
||||
public StringProperty labelProperty() {
|
||||
return labelProperty;
|
||||
}
|
||||
|
||||
public Server getServer() {
|
||||
return new Server(server.getUrl(), labelProperty.get().isEmpty() ? null : labelProperty.get());
|
||||
}
|
||||
}
|
||||
|
||||
private static class AliasCell extends TextFieldTableCell<ServerEntry, String> {
|
||||
public AliasCell() {
|
||||
super(new DefaultStringConverter());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commitEdit(String label) {
|
||||
if(label != null) {
|
||||
label = label.trim();
|
||||
}
|
||||
|
||||
// This block is necessary to support commit on losing focus, because
|
||||
// the baked-in mechanism sets our editing state to false before we can
|
||||
// intercept the loss of focus. The default commitEdit(...) method
|
||||
// simply bails if we are not editing...
|
||||
if (!isEditing() && !label.equals(getItem())) {
|
||||
TableView<ServerEntry> table = getTableView();
|
||||
if(table != null) {
|
||||
TableColumn<ServerEntry, String> column = getTableColumn();
|
||||
TableColumn.CellEditEvent<ServerEntry, String> event = new TableColumn.CellEditEvent<>(
|
||||
table, new TablePosition<>(table, getIndex(), column),
|
||||
TableColumn.editCommitEvent(), label
|
||||
);
|
||||
Event.fireEvent(column, event);
|
||||
}
|
||||
}
|
||||
|
||||
super.commitEdit(label);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startEdit() {
|
||||
super.startEdit();
|
||||
|
||||
try {
|
||||
Field f = getClass().getSuperclass().getDeclaredField("textField");
|
||||
f.setAccessible(true);
|
||||
TextField textField = (TextField)f.get(this);
|
||||
textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
|
||||
if (!isNowFocused) {
|
||||
commitEdit(getConverter().fromString(textField.getText()));
|
||||
setText(getConverter().fromString(textField.getText()));
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
-53
@@ -12,11 +12,13 @@ import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Server;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.text.Font;
|
||||
@@ -51,6 +53,8 @@ import java.util.Random;
|
||||
public class ServerPreferencesController extends PreferencesDetailController {
|
||||
private static final Logger log = LoggerFactory.getLogger(ServerPreferencesController.class);
|
||||
|
||||
private static final Server MANAGE_ALIASES_SERVER = new Server("tcp://localhost", "Manage Aliases...");
|
||||
|
||||
@FXML
|
||||
private ToggleGroup serverTypeToggleGroup;
|
||||
|
||||
@@ -79,7 +83,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
private Form coreForm;
|
||||
|
||||
@FXML
|
||||
private ComboBox<String> recentCoreServers;
|
||||
private ComboBox<Server> recentCoreServers;
|
||||
|
||||
@FXML
|
||||
private ComboBoxTextField coreHost;
|
||||
@@ -121,7 +125,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
private Form electrumForm;
|
||||
|
||||
@FXML
|
||||
private ComboBox<String> recentElectrumServers;
|
||||
private ComboBox<Server> recentElectrumServers;
|
||||
|
||||
@FXML
|
||||
private ComboBoxTextField electrumHost;
|
||||
@@ -267,26 +271,52 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
}
|
||||
});
|
||||
|
||||
recentCoreServers.setConverter(new UrlHostConverter());
|
||||
recentCoreServers.setItems(FXCollections.observableList(Config.get().getRecentCoreServers() == null ? new ArrayList<>() : Config.get().getRecentCoreServers()));
|
||||
recentCoreServers.setCellFactory(value -> new ServerCell());
|
||||
recentCoreServers.setItems(getObservableServerList(Config.get().getRecentCoreServers()));
|
||||
recentCoreServers.prefWidthProperty().bind(coreHost.widthProperty());
|
||||
recentCoreServers.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null && Protocol.getProtocol(newValue) != null) {
|
||||
HostAndPort hostAndPort = Protocol.getProtocol(newValue).getServerHostAndPort(newValue);
|
||||
coreHost.setText(hostAndPort.getHost());
|
||||
corePort.setText(Integer.toString(hostAndPort.getPort()));
|
||||
if(newValue != null) {
|
||||
if(newValue == MANAGE_ALIASES_SERVER) {
|
||||
ServerAliasDialog serverAliasDialog = new ServerAliasDialog(ServerType.BITCOIN_CORE);
|
||||
Optional<Server> optServer = serverAliasDialog.showAndWait();
|
||||
recentCoreServers.setItems(getObservableServerList(Config.get().getRecentCoreServers()));
|
||||
Server selectedServer = optServer.orElseGet(() -> Config.get().getCoreServer());
|
||||
Platform.runLater(() -> recentCoreServers.setValue(selectedServer));
|
||||
} else if(newValue.getHostAndPort() != null) {
|
||||
HostAndPort hostAndPort = newValue.getHostAndPort();
|
||||
corePort.setText(hostAndPort.hasPort() ? Integer.toString(hostAndPort.getPort()) : "");
|
||||
if(newValue.getAlias() != null) {
|
||||
coreHost.setText(newValue.getAlias());
|
||||
} else {
|
||||
coreHost.setText(hostAndPort.getHost());
|
||||
}
|
||||
coreHost.positionCaret(coreHost.getText().length());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
recentElectrumServers.setConverter(new UrlHostConverter());
|
||||
recentElectrumServers.setItems(FXCollections.observableList(Config.get().getRecentElectrumServers() == null ? new ArrayList<>() : Config.get().getRecentElectrumServers()));
|
||||
recentElectrumServers.setCellFactory(value -> new ServerCell());
|
||||
recentElectrumServers.setItems(getObservableServerList(Config.get().getRecentElectrumServers()));
|
||||
recentElectrumServers.prefWidthProperty().bind(electrumHost.widthProperty());
|
||||
recentElectrumServers.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null && Protocol.getProtocol(newValue) != null) {
|
||||
HostAndPort hostAndPort = Protocol.getProtocol(newValue).getServerHostAndPort(newValue);
|
||||
electrumHost.setText(hostAndPort.getHost());
|
||||
electrumPort.setText(Integer.toString(hostAndPort.getPort()));
|
||||
electrumUseSsl.setSelected(Protocol.getProtocol(newValue) == Protocol.SSL);
|
||||
if(newValue != null) {
|
||||
if(newValue == MANAGE_ALIASES_SERVER) {
|
||||
ServerAliasDialog serverAliasDialog = new ServerAliasDialog(ServerType.ELECTRUM_SERVER);
|
||||
Optional<Server> optServer = serverAliasDialog.showAndWait();
|
||||
recentElectrumServers.setItems(getObservableServerList(Config.get().getRecentElectrumServers()));
|
||||
Server selectedServer = optServer.orElseGet(() -> Config.get().getElectrumServer());
|
||||
Platform.runLater(() -> recentElectrumServers.setValue(selectedServer));
|
||||
} else if(newValue.getHostAndPort() != null) {
|
||||
HostAndPort hostAndPort = newValue.getHostAndPort();
|
||||
electrumPort.setText(hostAndPort.hasPort() ? Integer.toString(hostAndPort.getPort()) : "");
|
||||
electrumUseSsl.setSelected(newValue.getProtocol() == Protocol.SSL);
|
||||
if(newValue.getAlias() != null) {
|
||||
electrumHost.setText(newValue.getAlias());
|
||||
} else {
|
||||
electrumHost.setText(hostAndPort.getHost());
|
||||
}
|
||||
electrumHost.positionCaret(electrumHost.getText().length());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -339,7 +369,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
setTestResultsFont();
|
||||
testConnection.setOnAction(event -> {
|
||||
testConnection.setGraphic(getGlyph(FontAwesome5.Glyph.ELLIPSIS_H, null));
|
||||
testResults.setText("Connecting to " + config.getServerAddress() + "...");
|
||||
testResults.setText("Connecting " + (config.hasServer() ? "to " + config.getServer().getUrl() : "") + "...");
|
||||
|
||||
if(Config.get().requiresInternalTor() && Tor.getDefault() == null) {
|
||||
startTor();
|
||||
@@ -358,7 +388,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
testConnection.setVisible(true);
|
||||
});
|
||||
|
||||
PublicElectrumServer configPublicElectrumServer = PublicElectrumServer.fromUrl(config.getPublicElectrumServer());
|
||||
PublicElectrumServer configPublicElectrumServer = PublicElectrumServer.fromServer(config.getPublicElectrumServer());
|
||||
if(configPublicElectrumServer == null && PublicElectrumServer.supportedNetwork()) {
|
||||
List<PublicElectrumServer> servers = PublicElectrumServer.getServers();
|
||||
if(!servers.isEmpty()) {
|
||||
@@ -368,16 +398,16 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
publicElectrumServer.setValue(configPublicElectrumServer);
|
||||
}
|
||||
|
||||
String coreServer = config.getCoreServer();
|
||||
Server coreServer = config.getCoreServer();
|
||||
if(coreServer != null) {
|
||||
Protocol protocol = Protocol.getProtocol(coreServer);
|
||||
|
||||
if(protocol != null) {
|
||||
HostAndPort server = protocol.getServerHostAndPort(coreServer);
|
||||
coreHost.setText(server.getHost());
|
||||
if(server.hasPort()) {
|
||||
corePort.setText(Integer.toString(server.getPort()));
|
||||
}
|
||||
HostAndPort hostAndPort = coreServer.getHostAndPort();
|
||||
Server server = config.getRecentCoreServers().stream().filter(coreServer::equals).findFirst().orElse(null);
|
||||
if(server != null) {
|
||||
coreHost.setLeft(getGlyph(FontAwesome5.Glyph.TAG, null));
|
||||
}
|
||||
coreHost.setText(server == null || server.getAlias() == null ? hostAndPort.getHost() : server.getAlias());
|
||||
if(hostAndPort.hasPort()) {
|
||||
corePort.setText(Integer.toString(hostAndPort.getPort()));
|
||||
}
|
||||
} else {
|
||||
coreHost.setText("127.0.0.1");
|
||||
@@ -396,21 +426,22 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
}
|
||||
}
|
||||
|
||||
String electrumServer = config.getElectrumServer();
|
||||
Server electrumServer = config.getElectrumServer();
|
||||
if(electrumServer != null) {
|
||||
Protocol protocol = Protocol.getProtocol(electrumServer);
|
||||
Protocol protocol = electrumServer.getProtocol();
|
||||
boolean ssl = protocol.equals(Protocol.SSL);
|
||||
electrumUseSsl.setSelected(ssl);
|
||||
electrumCertificate.setDisable(!ssl);
|
||||
electrumCertificateSelect.setDisable(!ssl);
|
||||
|
||||
if(protocol != null) {
|
||||
boolean ssl = protocol.equals(Protocol.SSL);
|
||||
electrumUseSsl.setSelected(ssl);
|
||||
electrumCertificate.setDisable(!ssl);
|
||||
electrumCertificateSelect.setDisable(!ssl);
|
||||
|
||||
HostAndPort server = protocol.getServerHostAndPort(electrumServer);
|
||||
electrumHost.setText(server.getHost());
|
||||
if(server.hasPort()) {
|
||||
electrumPort.setText(Integer.toString(server.getPort()));
|
||||
}
|
||||
HostAndPort hostAndPort = electrumServer.getHostAndPort();
|
||||
Server server = config.getRecentElectrumServers().stream().filter(electrumServer::equals).findFirst().orElse(null);
|
||||
if(server != null) {
|
||||
electrumHost.setLeft(getGlyph(FontAwesome5.Glyph.TAG, null));
|
||||
}
|
||||
electrumHost.setText(server == null || server.getAlias() == null ? hostAndPort.getHost() : server.getAlias());
|
||||
if(hostAndPort.hasPort()) {
|
||||
electrumPort.setText(Integer.toString(hostAndPort.getPort()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -449,7 +480,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
torService.setOnSucceeded(workerStateEvent -> {
|
||||
Tor.setDefault(torService.getValue());
|
||||
torService.cancel();
|
||||
testResults.appendText("\nTor running, connecting to " + Config.get().getServerAddress() + "...");
|
||||
testResults.appendText("\nTor running, connecting to " + Config.get().getServer().getUrl() + "...");
|
||||
startElectrumConnection();
|
||||
});
|
||||
torService.setOnFailed(workerStateEvent -> {
|
||||
@@ -495,6 +526,13 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
Config.get().setMode(Mode.ONLINE);
|
||||
connectionService.cancel();
|
||||
useProxyOriginal = null;
|
||||
if(Config.get().addRecentServer()) {
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
recentCoreServers.setItems(getObservableServerList(Config.get().getRecentCoreServers()));
|
||||
} else if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
|
||||
recentElectrumServers.setItems(getObservableServerList(Config.get().getRecentElectrumServers()));
|
||||
}
|
||||
}
|
||||
});
|
||||
connectionService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().unregister(connectionService);
|
||||
@@ -668,24 +706,32 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
@NotNull
|
||||
private ChangeListener<PublicElectrumServer> getPublicElectrumServerListener(Config config) {
|
||||
return (observable, oldValue, newValue) -> {
|
||||
config.setPublicElectrumServer(newValue.getUrl());
|
||||
config.setPublicElectrumServer(newValue.getServer());
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ChangeListener<String> getBitcoinCoreListener(Config config) {
|
||||
return (observable, oldValue, newValue) -> {
|
||||
Server existingServer = config.getRecentCoreServers().stream().filter(server -> coreHost.getText().equals(server.getAlias())).findFirst().orElse(null);
|
||||
coreHost.setLeft(existingServer == null ? null : getGlyph(FontAwesome5.Glyph.TAG, null));
|
||||
setCoreServerInConfig(config);
|
||||
};
|
||||
}
|
||||
|
||||
private void setCoreServerInConfig(Config config) {
|
||||
Server existingServer = config.getRecentCoreServers().stream().filter(server -> coreHost.getText().equals(server.getAlias())).findFirst().orElse(null);
|
||||
if(existingServer != null) {
|
||||
config.setCoreServer(existingServer);
|
||||
return;
|
||||
}
|
||||
|
||||
String hostAsString = getHost(coreHost.getText());
|
||||
Integer portAsInteger = getPort(corePort.getText());
|
||||
if(hostAsString != null && portAsInteger != null && isValidPort(portAsInteger)) {
|
||||
config.setCoreServer(Protocol.HTTP.toUrlString(hostAsString, portAsInteger));
|
||||
config.setCoreServer(new Server(Protocol.HTTP.toUrlString(hostAsString, portAsInteger)));
|
||||
} else if(hostAsString != null) {
|
||||
config.setCoreServer(Protocol.HTTP.toUrlString(hostAsString));
|
||||
config.setCoreServer(new Server(Protocol.HTTP.toUrlString(hostAsString)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,17 +745,25 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
@NotNull
|
||||
private ChangeListener<String> getElectrumServerListener(Config config) {
|
||||
return (observable, oldValue, newValue) -> {
|
||||
Server existingServer = config.getRecentElectrumServers().stream().filter(server -> electrumHost.getText().equals(server.getAlias())).findFirst().orElse(null);
|
||||
electrumHost.setLeft(existingServer == null ? null : getGlyph(FontAwesome5.Glyph.TAG, null));
|
||||
setElectrumServerInConfig(config);
|
||||
};
|
||||
}
|
||||
|
||||
private void setElectrumServerInConfig(Config config) {
|
||||
Server existingServer = config.getRecentElectrumServers().stream().filter(server -> electrumHost.getText().equals(server.getAlias())).findFirst().orElse(null);
|
||||
if(existingServer != null) {
|
||||
config.setElectrumServer(existingServer);
|
||||
return;
|
||||
}
|
||||
|
||||
String hostAsString = getHost(electrumHost.getText());
|
||||
Integer portAsInteger = getPort(electrumPort.getText());
|
||||
if(hostAsString != null && portAsInteger != null && isValidPort(portAsInteger)) {
|
||||
config.setElectrumServer(getProtocol().toUrlString(hostAsString, portAsInteger));
|
||||
config.setElectrumServer(new Server(getProtocol().toUrlString(hostAsString, portAsInteger)));
|
||||
} else if(hostAsString != null) {
|
||||
config.setElectrumServer(getProtocol().toUrlString(hostAsString));
|
||||
config.setElectrumServer(new Server(getProtocol().toUrlString(hostAsString)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -777,7 +831,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
}
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName, String styleClass) {
|
||||
private static Glyph getGlyph(FontAwesome5.Glyph glyphName, String styleClass) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(12);
|
||||
if(styleClass != null) {
|
||||
@@ -813,6 +867,12 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
}
|
||||
}
|
||||
|
||||
private ObservableList<Server> getObservableServerList(List<Server> servers) {
|
||||
ObservableList<Server> serverObservableList = FXCollections.observableList(new ArrayList<>(servers));
|
||||
serverObservableList.add(MANAGE_ALIASES_SERVER);
|
||||
return serverObservableList;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtStatus(BwtStatusEvent event) {
|
||||
if(!(event instanceof BwtSyncStatusEvent)) {
|
||||
@@ -844,15 +904,28 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
});
|
||||
}
|
||||
|
||||
private static class UrlHostConverter extends StringConverter<String> {
|
||||
private static class ServerCell extends ListCell<Server> {
|
||||
@Override
|
||||
public String toString(String serverUrl) {
|
||||
return serverUrl == null || Protocol.getProtocol(serverUrl) == null ? "" : Protocol.getProtocol(serverUrl).getServerHostAndPort(serverUrl).getHost();
|
||||
}
|
||||
protected void updateItem(Server server, boolean empty) {
|
||||
super.updateItem(server, empty);
|
||||
if(server == null || empty) {
|
||||
setText("");
|
||||
setGraphic(null);
|
||||
} else {
|
||||
String serverAlias = server.getAlias();
|
||||
|
||||
@Override
|
||||
public String fromString(String string) {
|
||||
return null;
|
||||
if(server == MANAGE_ALIASES_SERVER) {
|
||||
setText(serverAlias);
|
||||
setStyle("-fx-font-style: italic");
|
||||
setGraphic(null);
|
||||
} else if(serverAlias != null) {
|
||||
setText(serverAlias);
|
||||
setGraphic(getGlyph(FontAwesome5.Glyph.TAG, null));
|
||||
} else {
|
||||
setText(server.getHost());
|
||||
setGraphic(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,6 +247,7 @@ public class CounterpartyController extends SorobanController {
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(responseMessage -> {
|
||||
requestUserAttention();
|
||||
if(accepted) {
|
||||
startCounterpartyCollaboration(counterpartyCahootsWallet, paymentCodeInitiator, cahootsType);
|
||||
followPaymentCode(paymentCodeInitiator);
|
||||
@@ -254,10 +255,12 @@ public class CounterpartyController extends SorobanController {
|
||||
}, error -> {
|
||||
log.error("Error sending meeting response", error);
|
||||
mixingPartner.setVisible(false);
|
||||
requestUserAttention();
|
||||
});
|
||||
}, error -> {
|
||||
log.error("Failed to receive meeting request", error);
|
||||
mixingPartner.setVisible(false);
|
||||
requestUserAttention();
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Error sending meeting response", e);
|
||||
|
||||
@@ -105,6 +105,9 @@ public class InitiatorController extends SorobanController {
|
||||
@FXML
|
||||
private Label step2Desc;
|
||||
|
||||
@FXML
|
||||
private Hyperlink meetingFail;
|
||||
|
||||
@FXML
|
||||
private ProgressBar sorobanProgressBar;
|
||||
|
||||
@@ -188,6 +191,17 @@ public class InitiatorController extends SorobanController {
|
||||
broadcastSuccessful.managedProperty().bind(broadcastSuccessful.visibleProperty());
|
||||
broadcastSuccessful.setVisible(false);
|
||||
|
||||
meetingFail.managedProperty().bind(meetingFail.visibleProperty());
|
||||
meetingFail.setVisited(true);
|
||||
meetingFail.setVisible(false);
|
||||
meetingFail.setOnAction(event -> {
|
||||
meetingFail.setVisible(false);
|
||||
step2Desc.setText("Retrying...");
|
||||
sorobanProgressLabel.setVisible(true);
|
||||
startInitiatorMeetingRequest(AppServices.getSorobanServices().getSoroban(walletId), wallet);
|
||||
step2Timer.start();
|
||||
});
|
||||
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(false);
|
||||
@@ -409,6 +423,7 @@ public class InitiatorController extends SorobanController {
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(sorobanResponse -> {
|
||||
requestUserAttention();
|
||||
if(sorobanResponse.isAccept()) {
|
||||
sorobanProgressBar.setProgress(0.1);
|
||||
sorobanProgressLabel.setText("Mix partner accepted!");
|
||||
@@ -419,17 +434,17 @@ public class InitiatorController extends SorobanController {
|
||||
}
|
||||
}, error -> {
|
||||
log.error("Error receiving meeting response", error);
|
||||
String cutFrom = "Exception: ";
|
||||
int index = error.getMessage().lastIndexOf(cutFrom);
|
||||
String msg = index < 0 ? error.getMessage() : error.getMessage().substring(index + cutFrom.length());
|
||||
msg = msg.replace("#Cahoots", "mix transaction");
|
||||
step2Desc.setText(msg);
|
||||
step2Desc.setText(getErrorMessage(error));
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
meetingFail.setVisible(true);
|
||||
requestUserAttention();
|
||||
});
|
||||
}, error -> {
|
||||
log.error("Error sending meeting request", error);
|
||||
step2Desc.setText(error.getMessage());
|
||||
step2Desc.setText(getErrorMessage(error));
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
meetingFail.setVisible(true);
|
||||
requestUserAttention();
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Error sending meeting request", e);
|
||||
@@ -512,9 +527,7 @@ public class InitiatorController extends SorobanController {
|
||||
},
|
||||
error -> {
|
||||
log.error("Error creating mix transaction", error);
|
||||
String cutFrom = "Exception: ";
|
||||
int index = error.getMessage().lastIndexOf(cutFrom);
|
||||
step2Desc.setText(index < 0 ? error.getMessage() : error.getMessage().substring(index + cutFrom.length()));
|
||||
step2Desc.setText(getErrorMessage(error));
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
});
|
||||
} catch(Exception e) {
|
||||
@@ -667,6 +680,15 @@ public class InitiatorController extends SorobanController {
|
||||
}
|
||||
}
|
||||
|
||||
private static String getErrorMessage(Throwable error) {
|
||||
String cutFrom = "Exception: ";
|
||||
int index = error.getMessage().lastIndexOf(cutFrom);
|
||||
String msg = index < 0 ? error.getMessage() : error.getMessage().substring(index + cutFrom.length());
|
||||
msg = msg.replace("#Cahoots", "mix transaction");
|
||||
msg = msg.endsWith(".") ? msg : msg + ".";
|
||||
return msg;
|
||||
}
|
||||
|
||||
public boolean isTransactionAccepted() {
|
||||
return transactionAccepted.get() == Boolean.TRUE;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ public class InitiatorDialog extends Dialog<Transaction> {
|
||||
dialogPane.setPrefHeight(530);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/initiator.css").toExternalForm());
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.Taskbar;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SorobanController {
|
||||
@@ -120,4 +120,10 @@ public class SorobanController {
|
||||
|
||||
return feeAmt;
|
||||
}
|
||||
|
||||
protected void requestUserAttention() {
|
||||
if(Taskbar.isTaskbarSupported() && Taskbar.getTaskbar().isSupported(Taskbar.Feature.USER_ATTENTION)) {
|
||||
Taskbar.getTaskbar().requestUserAttention(true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.sparrowwallet.sparrow.terminal;
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize;
|
||||
import com.googlecode.lanterna.graphics.ThemeDefinition;
|
||||
import com.googlecode.lanterna.gui2.ComponentRenderer;
|
||||
import com.googlecode.lanterna.gui2.ProgressBar;
|
||||
import com.googlecode.lanterna.gui2.TextGUIGraphics;
|
||||
|
||||
public class BackgroundProgressBarRenderer implements ComponentRenderer<ProgressBar> {
|
||||
@Override
|
||||
public TerminalSize getPreferredSize(ProgressBar component) {
|
||||
int preferredWidth = component.getPreferredWidth();
|
||||
if(preferredWidth > 0) {
|
||||
return new TerminalSize(preferredWidth, 1);
|
||||
} else {
|
||||
return new TerminalSize(10, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawComponent(TextGUIGraphics graphics, ProgressBar component) {
|
||||
TerminalSize size = graphics.getSize();
|
||||
if(size.getRows() == 0 || size.getColumns() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ThemeDefinition themeDefinition = component.getThemeDefinition();
|
||||
int columnOfProgress = (int)(component.getProgress() * size.getColumns());
|
||||
for(int row = 0; row < size.getRows(); row++) {
|
||||
graphics.applyThemeStyle(themeDefinition.getActive());
|
||||
for(int column = 0; column < size.getColumns(); column++) {
|
||||
if(column < columnOfProgress) {
|
||||
graphics.setCharacter(column, row, themeDefinition.getCharacter("FILLER", ' '));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.sparrowwallet.sparrow.terminal;
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize;
|
||||
import com.googlecode.lanterna.gui2.ActionListBox;
|
||||
import com.googlecode.lanterna.gui2.dialogs.ActionListDialogBuilder;
|
||||
import com.googlecode.lanterna.gui2.dialogs.FileDialogBuilder;
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.StorageEvent;
|
||||
import com.sparrowwallet.sparrow.event.TimedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.terminal.preferences.GeneralDialog;
|
||||
import com.sparrowwallet.sparrow.terminal.preferences.ServerStatusDialog;
|
||||
import com.sparrowwallet.sparrow.terminal.preferences.ServerTypeDialog;
|
||||
import com.sparrowwallet.sparrow.terminal.wallet.Bip39Dialog;
|
||||
import com.sparrowwallet.sparrow.terminal.wallet.LoadWallet;
|
||||
import com.sparrowwallet.sparrow.terminal.wallet.WatchOnlyDialog;
|
||||
import javafx.application.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
|
||||
public class MasterActionListBox extends ActionListBox {
|
||||
private static final Logger log = LoggerFactory.getLogger(MasterActionListBox.class);
|
||||
|
||||
public static final int MAX_RECENT_WALLETS = 6;
|
||||
|
||||
public MasterActionListBox(SparrowTerminal sparrowTerminal) {
|
||||
super(new TerminalSize(14, 3));
|
||||
|
||||
addItem("Wallets", () -> {
|
||||
ActionListDialogBuilder builder = new ActionListDialogBuilder();
|
||||
builder.setTitle("Wallets");
|
||||
if(Config.get().getRecentWalletFiles() != null) {
|
||||
for(int i = 0; i < Config.get().getRecentWalletFiles().size() && i < MAX_RECENT_WALLETS; i++) {
|
||||
File recentWalletFile = Config.get().getRecentWalletFiles().get(i);
|
||||
if(!recentWalletFile.exists()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Storage storage = new Storage(recentWalletFile);
|
||||
|
||||
Optional<Wallet> optWallet = AppServices.get().getOpenWallets().entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getWalletFile().equals(recentWalletFile)).map(Map.Entry::getKey)
|
||||
.map(wallet -> wallet.isMasterWallet() ? wallet : wallet.getMasterWallet()).findFirst();
|
||||
if(optWallet.isPresent()) {
|
||||
Wallet wallet = optWallet.get();
|
||||
Storage existingStorage = AppServices.get().getOpenWallets().get(wallet);
|
||||
builder.addAction(storage.getWalletName(null) + "*", () -> openLoadedWallet(existingStorage, optWallet.get()));
|
||||
} else {
|
||||
builder.addAction(storage.getWalletName(null), new LoadWallet(storage));
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.addAction("Open Wallet...", () -> {
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(MasterActionListBox::openWallet);
|
||||
});
|
||||
builder.addAction("Create Wallet...", () -> {
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(MasterActionListBox::createWallet);
|
||||
});
|
||||
builder.build().showDialog(SparrowTerminal.get().getGui());
|
||||
});
|
||||
|
||||
addItem("Preferences", () -> {
|
||||
new ActionListDialogBuilder()
|
||||
.setTitle("Preferences")
|
||||
.addAction("General", () -> {
|
||||
GeneralDialog generalDialog = new GeneralDialog();
|
||||
generalDialog.showDialog(sparrowTerminal.getGui());
|
||||
})
|
||||
.addAction("Server", () -> {
|
||||
if(Config.get().hasServer()) {
|
||||
ServerStatusDialog serverStatusDialog = new ServerStatusDialog();
|
||||
serverStatusDialog.showDialog(sparrowTerminal.getGui());
|
||||
} else {
|
||||
ServerTypeDialog serverTypeDialog = new ServerTypeDialog();
|
||||
serverTypeDialog.showDialog(sparrowTerminal.getGui());
|
||||
}
|
||||
})
|
||||
.build()
|
||||
.showDialog(sparrowTerminal.getGui());
|
||||
});
|
||||
|
||||
addItem("Quit", () -> sparrowTerminal.getGui().getMainWindow().close());
|
||||
}
|
||||
|
||||
private static void openLoadedWallet(Storage storage, Wallet wallet) {
|
||||
if(SparrowTerminal.get().isLocked(storage)) {
|
||||
String walletId = storage.getWalletId(wallet);
|
||||
|
||||
TextInputDialogBuilder builder = new TextInputDialogBuilder().setTitle("Wallet Password");
|
||||
builder.setDescription("Enter the wallet password:");
|
||||
builder.setPasswordInput(true);
|
||||
|
||||
String password = builder.build().showDialog(SparrowTerminal.get().getGui());
|
||||
if(password != null) {
|
||||
Platform.runLater(() -> {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, new SecureString(password), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||
keyDerivationService.getValue().clear();
|
||||
SparrowTerminal.get().unlockWallet(storage);
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> LoadWallet.getOpeningDialog(storage, wallet).showDialog(SparrowTerminal.get().getGui()));
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
showErrorDialog("Invalid Password", "The wallet password was invalid.");
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> LoadWallet.getOpeningDialog(storage, wallet).showDialog(SparrowTerminal.get().getGui()));
|
||||
}
|
||||
}
|
||||
|
||||
private static void openWallet() {
|
||||
FileDialogBuilder openBuilder = new FileDialogBuilder().setTitle("Open Wallet");
|
||||
openBuilder.setShowHiddenDirectories(true);
|
||||
openBuilder.setSelectedFile(Storage.getWalletsDir());
|
||||
File file = openBuilder.build().showDialog(SparrowTerminal.get().getGui());
|
||||
if(file != null) {
|
||||
LoadWallet loadWallet = new LoadWallet(new Storage(file));
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(loadWallet);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createWallet() {
|
||||
TextInputDialogBuilder newWalletNameBuilder = new TextInputDialogBuilder();
|
||||
newWalletNameBuilder.setTitle("Create Wallet");
|
||||
newWalletNameBuilder.setDescription("Enter a name for the wallet");
|
||||
newWalletNameBuilder.setValidator(content -> content.isEmpty() ? "Please enter a name" : (Storage.walletExists(content) ? "Wallet already exists" : null));
|
||||
String walletName = newWalletNameBuilder.build().showDialog(SparrowTerminal.get().getGui());
|
||||
|
||||
ActionListDialogBuilder newBuilder = new ActionListDialogBuilder();
|
||||
newBuilder.setTitle("Create Wallet");
|
||||
newBuilder.setDescription("Choose the type of wallet");
|
||||
newBuilder.addAction("Software (BIP39)", () -> {
|
||||
Bip39Dialog bip39Dialog = new Bip39Dialog(walletName);
|
||||
bip39Dialog.showDialog(SparrowTerminal.get().getGui());
|
||||
});
|
||||
newBuilder.addAction("Watch Only", () -> {
|
||||
WatchOnlyDialog watchOnlyDialog = new WatchOnlyDialog(walletName);
|
||||
watchOnlyDialog.showDialog(SparrowTerminal.get().getGui());
|
||||
});
|
||||
newBuilder.build().showDialog(SparrowTerminal.get().getGui());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.sparrowwallet.sparrow.terminal;
|
||||
|
||||
import com.googlecode.lanterna.gui2.BasicWindow;
|
||||
import com.googlecode.lanterna.gui2.BorderLayout;
|
||||
import com.googlecode.lanterna.gui2.Panel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MasterActionWindow extends BasicWindow {
|
||||
public MasterActionWindow(SparrowTerminal sparrowTerminal) {
|
||||
setHints(List.of(Hint.CENTERED));
|
||||
|
||||
Panel panel = new Panel(new BorderLayout());
|
||||
|
||||
MasterActionListBox masterActionListBox = new MasterActionListBox(sparrowTerminal);
|
||||
panel.addComponent(masterActionListBox, BorderLayout.Location.CENTER);
|
||||
|
||||
setComponent(panel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.sparrowwallet.sparrow.terminal;
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize;
|
||||
import com.googlecode.lanterna.gui2.EmptySpace;
|
||||
import com.googlecode.lanterna.gui2.Label;
|
||||
import com.googlecode.lanterna.gui2.LinearLayout;
|
||||
import com.googlecode.lanterna.gui2.Panel;
|
||||
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class ModalDialog extends DialogWindow {
|
||||
public ModalDialog(String walletName, String description) {
|
||||
super(walletName);
|
||||
|
||||
setHints(List.of(Hint.CENTERED));
|
||||
setFixedSize(new TerminalSize(30, 5));
|
||||
|
||||
Panel mainPanel = new Panel();
|
||||
mainPanel.setLayoutManager(new LinearLayout());
|
||||
mainPanel.addComponent(new EmptySpace(), LinearLayout.createLayoutData(LinearLayout.Alignment.Beginning, LinearLayout.GrowPolicy.CanGrow));
|
||||
|
||||
Label label = new Label(description);
|
||||
mainPanel.addComponent(label, LinearLayout.createLayoutData(LinearLayout.Alignment.Center));
|
||||
|
||||
mainPanel.addComponent(new EmptySpace(), LinearLayout.createLayoutData(LinearLayout.Alignment.Beginning, LinearLayout.GrowPolicy.CanGrow));
|
||||
|
||||
setComponent(mainPanel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.sparrowwallet.sparrow.terminal;
|
||||
|
||||
import com.googlecode.lanterna.TextColor;
|
||||
import com.googlecode.lanterna.gui2.DefaultWindowManager;
|
||||
import com.googlecode.lanterna.gui2.EmptySpace;
|
||||
import com.googlecode.lanterna.gui2.TextGUIThread;
|
||||
import com.googlecode.lanterna.screen.Screen;
|
||||
import com.googlecode.lanterna.screen.TerminalScreen;
|
||||
import com.googlecode.lanterna.terminal.DefaultTerminalFactory;
|
||||
import com.googlecode.lanterna.terminal.Terminal;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.*;
|
||||
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletOpenedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletOpeningEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.terminal.wallet.WalletData;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.terminal.MasterActionListBox.MAX_RECENT_WALLETS;
|
||||
|
||||
public class SparrowTerminal extends Application {
|
||||
private static final Logger log = LoggerFactory.getLogger(SparrowTerminal.class);
|
||||
|
||||
private static SparrowTerminal sparrowTerminal;
|
||||
|
||||
private Terminal terminal;
|
||||
private Screen screen;
|
||||
private SparrowTextGui gui;
|
||||
|
||||
private final Map<String, WalletData> walletData = new HashMap<>();
|
||||
private final Set<File> lockedWallets = new HashSet<>();
|
||||
|
||||
private static final javafx.stage.Window DEFAULT_WINDOW = new Window() { };
|
||||
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||
log.error("Exception in thread \"" + t.getName() + "\"", e);
|
||||
});
|
||||
|
||||
AppServices.initialize(this, new TerminalInteractionServices());
|
||||
|
||||
this.terminal = new DefaultTerminalFactory().createTerminal();
|
||||
this.screen = new TerminalScreen(terminal);
|
||||
this.gui = new SparrowTextGui(this, screen, new DefaultWindowManager(), new EmptySpace(TextColor.ANSI.BLUE));
|
||||
EventManager.get().register(gui);
|
||||
|
||||
sparrowTerminal = this;
|
||||
|
||||
try {
|
||||
getScreen().startScreen();
|
||||
getGui().getMainWindow().waitUntilClosed();
|
||||
} finally {
|
||||
exit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) throws Exception {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
|
||||
}
|
||||
|
||||
public Screen getScreen() {
|
||||
return screen;
|
||||
}
|
||||
|
||||
public SparrowTextGui getGui() {
|
||||
return gui;
|
||||
}
|
||||
|
||||
public TextGUIThread getGuiThread() {
|
||||
return gui.getGUIThread();
|
||||
}
|
||||
|
||||
public Map<String, WalletData> getWalletData() {
|
||||
return walletData;
|
||||
}
|
||||
|
||||
public void exit() {
|
||||
try {
|
||||
screen.stopScreen();
|
||||
Platform.runLater(() -> {
|
||||
AppServices.get().stop();
|
||||
Platform.exit();
|
||||
});
|
||||
SparrowWallet.Instance instance = SparrowWallet.getSparrowInstance();
|
||||
if(instance != null) {
|
||||
instance.freeLock();
|
||||
}
|
||||
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null && !recentWalletFiles.isEmpty()) {
|
||||
Set<File> openedWalletFiles = new LinkedHashSet<>(recentWalletFiles);
|
||||
openedWalletFiles.removeIf(file -> walletData.values().stream().noneMatch(data -> data.getWalletForm().getWalletFile().equals(file)));
|
||||
openedWalletFiles.addAll(Config.get().getRecentWalletFiles().subList(0, Math.min(3, recentWalletFiles.size())));
|
||||
Config.get().setRecentWalletFiles(new ArrayList<>(openedWalletFiles));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Could not stop terminal screen", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static SparrowTerminal get() {
|
||||
return sparrowTerminal;
|
||||
}
|
||||
|
||||
public static void addWallet(Storage storage, Wallet wallet) {
|
||||
if(wallet.isNested()) {
|
||||
WalletData walletData = SparrowTerminal.get().getWalletData().get(storage.getWalletId(wallet.getMasterWallet()));
|
||||
WalletForm walletForm = new WalletForm(storage, wallet);
|
||||
EventManager.get().register(walletForm);
|
||||
walletData.getWalletForm().getNestedWalletForms().add(walletForm);
|
||||
} else {
|
||||
EventManager.get().post(new WalletOpeningEvent(storage, wallet));
|
||||
|
||||
WalletForm walletForm = new WalletForm(storage, wallet);
|
||||
EventManager.get().register(walletForm);
|
||||
SparrowTerminal.get().getWalletData().put(walletForm.getWalletId(), new WalletData(walletForm));
|
||||
|
||||
List<WalletTabData> walletTabDataList = SparrowTerminal.get().getWalletData().values().stream()
|
||||
.map(data -> new WalletTabData(TabData.TabType.WALLET, data.getWalletForm())).collect(Collectors.toList());
|
||||
EventManager.get().post(new OpenWalletsEvent(DEFAULT_WINDOW, walletTabDataList));
|
||||
|
||||
Set<File> walletFiles = new LinkedHashSet<>();
|
||||
walletFiles.add(storage.getWalletFile());
|
||||
if(Config.get().getRecentWalletFiles() != null) {
|
||||
walletFiles.addAll(Config.get().getRecentWalletFiles().stream().limit(MAX_RECENT_WALLETS - 1).collect(Collectors.toList()));
|
||||
}
|
||||
Config.get().setRecentWalletFiles(Config.get().isLoadRecentWallets() ? new ArrayList<>(walletFiles) : Collections.emptyList());
|
||||
}
|
||||
|
||||
EventManager.get().post(new WalletOpenedEvent(storage, wallet));
|
||||
}
|
||||
|
||||
public boolean isLocked(Storage storage) {
|
||||
return lockedWallets.contains(storage.getWalletFile());
|
||||
}
|
||||
|
||||
public void lockWallet(Storage storage) {
|
||||
lockedWallets.add(storage.getWalletFile());
|
||||
}
|
||||
|
||||
public void unlockWallet(Storage storage) {
|
||||
lockedWallets.remove(storage.getWalletFile());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.sparrowwallet.sparrow.terminal;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.googlecode.lanterna.TerminalSize;
|
||||
import com.googlecode.lanterna.TextColor;
|
||||
import com.googlecode.lanterna.gui2.*;
|
||||
import com.googlecode.lanterna.screen.Screen;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import javafx.animation.*;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class SparrowTextGui extends MultiWindowTextGUI {
|
||||
private final BasicWindow mainWindow;
|
||||
|
||||
private final Label connectedLabel;
|
||||
private final Label statusLabel;
|
||||
private final ProgressBar statusProgress;
|
||||
|
||||
private PauseTransition wait;
|
||||
private Timeline statusTimeline;
|
||||
private final DoubleProperty progressProperty = new SimpleDoubleProperty();
|
||||
|
||||
public SparrowTextGui(SparrowTerminal sparrowTerminal, Screen screen, WindowManager windowManager, Component background) {
|
||||
super(screen, windowManager, background);
|
||||
|
||||
this.mainWindow = new MasterActionWindow(sparrowTerminal);
|
||||
addWindow(mainWindow);
|
||||
|
||||
Panel panel = new Panel(new BorderLayout());
|
||||
|
||||
Panel titleBar = new Panel(new GridLayout(2));
|
||||
new Label("Sparrow Terminal").addTo(titleBar);
|
||||
this.connectedLabel = new Label("Disconnected");
|
||||
titleBar.addComponent(connectedLabel, GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, true, false));
|
||||
panel.addComponent(titleBar, BorderLayout.Location.TOP);
|
||||
|
||||
panel.addComponent(new EmptySpace(TextColor.ANSI.BLUE));
|
||||
|
||||
Panel statusBar = new Panel(new GridLayout(2));
|
||||
this.statusLabel = new Label("").addTo(statusBar);
|
||||
this.statusProgress = new ProgressBar(0, 100, 10);
|
||||
statusBar.addComponent(statusProgress, GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER, true, false));
|
||||
statusProgress.setRenderer(new BackgroundProgressBarRenderer());
|
||||
statusProgress.setLabelFormat(null);
|
||||
progressProperty.addListener((observable, oldValue, newValue) -> statusProgress.setValue((int) (newValue.doubleValue() * 100)));
|
||||
|
||||
panel.addComponent(statusBar, BorderLayout.Location.BOTTOM);
|
||||
getBackgroundPane().setComponent(panel);
|
||||
|
||||
getMainWindow().addWindowListener(new WindowListenerAdapter() {
|
||||
@Override
|
||||
public void onResized(Window window, TerminalSize oldSize, TerminalSize newSize) {
|
||||
if(!Objects.equals(oldSize, newSize)) {
|
||||
titleBar.invalidate();
|
||||
statusBar.invalidate();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
AppServices.get().start();
|
||||
}
|
||||
|
||||
public BasicWindow getMainWindow() {
|
||||
return mainWindow;
|
||||
}
|
||||
|
||||
private void setConnectedLabel(boolean connected) {
|
||||
getGUIThread().invokeLater(() -> {
|
||||
connectedLabel.setText(connected ? "Connected" : "Disconnected");
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void connectionStart(ConnectionStartEvent event) {
|
||||
statusUpdated(new StatusEvent(event.getStatus(), 120));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void connectionFailed(ConnectionFailedEvent event) {
|
||||
setConnectedLabel(false);
|
||||
statusUpdated(new StatusEvent("Connection failed: " + event.getMessage()));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void connection(ConnectionEvent event) {
|
||||
setConnectedLabel(true);
|
||||
statusUpdated(new StatusEvent("Connected to " + Config.get().getServerDisplayName() + " at height " + event.getBlockHeight()));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void disconnection(DisconnectionEvent event) {
|
||||
if(!AppServices.isConnecting() && !AppServices.isConnected()) {
|
||||
setConnectedLabel(false);
|
||||
statusUpdated(new StatusEvent("Disconnected"));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void statusUpdated(StatusEvent event) {
|
||||
getGUIThread().invokeLater(() -> statusLabel.setText(event.getStatus()));
|
||||
|
||||
if(wait != null && wait.getStatus() == Animation.Status.RUNNING) {
|
||||
wait.stop();
|
||||
}
|
||||
wait = new PauseTransition(Duration.seconds(event.getShowDuration()));
|
||||
wait.setOnFinished((e) -> {
|
||||
if(statusLabel.getText().equals(event.getStatus())) {
|
||||
getGUIThread().invokeLater(() -> statusLabel.setText(""));
|
||||
}
|
||||
});
|
||||
wait.play();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void timedEvent(TimedEvent event) {
|
||||
if(event.getTimeMills() == 0) {
|
||||
getGUIThread().invokeLater(() -> {
|
||||
statusLabel.setText("");
|
||||
statusProgress.setValue(0);
|
||||
});
|
||||
} else if(event.getTimeMills() < 0) {
|
||||
getGUIThread().invokeLater(() -> {
|
||||
statusLabel.setText(event.getStatus());
|
||||
statusProgress.setValue(0);
|
||||
});
|
||||
} else {
|
||||
getGUIThread().invokeLater(() -> {
|
||||
statusLabel.setText(event.getStatus());
|
||||
});
|
||||
statusTimeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(progressProperty, 0)),
|
||||
new KeyFrame(Duration.millis(event.getTimeMills()), e -> {
|
||||
getGUIThread().invokeLater(() -> {
|
||||
statusLabel.setText("");
|
||||
statusProgress.setValue(0);
|
||||
});
|
||||
}, new KeyValue(progressProperty, 1))
|
||||
);
|
||||
statusTimeline.setCycleCount(1);
|
||||
statusTimeline.play();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryStarted(WalletHistoryStartedEvent event) {
|
||||
statusUpdated(new StatusEvent("Loading wallet history..."));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryFinished(WalletHistoryFinishedEvent event) {
|
||||
if(statusLabel.getText().equals("Loading wallet history...")) {
|
||||
getGUIThread().invokeLater(() -> statusLabel.setText(""));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
|
||||
walletHistoryFinished(new WalletHistoryFinishedEvent(event.getWallet()));
|
||||
statusUpdated(new StatusEvent("Error retrieving wallet history" + (Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? ", trying another server..." : "")));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void childWalletsAdded(ChildWalletsAddedEvent event) {
|
||||
if(!event.getChildWallets().isEmpty()) {
|
||||
for(Wallet childWallet : event.getChildWallets()) {
|
||||
SparrowTerminal.addWallet(event.getStorage(), childWallet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.sparrowwallet.sparrow.terminal;
|
||||
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogBuilder;
|
||||
import com.googlecode.lanterna.gui2.dialogs.MessageDialogButton;
|
||||
import com.googlecode.lanterna.gui2.dialogs.TextInputDialogBuilder;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.sparrow.InteractionServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
|
||||
import java.text.BreakIterator;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
public class TerminalInteractionServices implements InteractionServices {
|
||||
private final Object alertShowing = new Object();
|
||||
private final Object passphraseShowing = new Object();
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Optional<ButtonType> showAlert(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
|
||||
if(Platform.isFxApplicationThread()) {
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
|
||||
Optional<ButtonType> optButtonType = showMessageDialog(title, content, buttons);
|
||||
Platform.runLater(() -> Platform.exitNestedEventLoop(alertShowing, optButtonType));
|
||||
});
|
||||
return (Optional<ButtonType>)Platform.enterNestedEventLoop(alertShowing);
|
||||
} else {
|
||||
return showMessageDialog(title, content, buttons);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<ButtonType> showMessageDialog(String title, String content, ButtonType[] buttons) {
|
||||
String formattedContent = formatLines(content, 50);
|
||||
|
||||
MessageDialogBuilder builder = new MessageDialogBuilder().setTitle(title).setText("\n" + formattedContent);
|
||||
for(ButtonType buttonType : buttons) {
|
||||
builder.addButton(getButton(buttonType));
|
||||
}
|
||||
|
||||
MessageDialogButton button = builder.build().showDialog(SparrowTerminal.get().getGui());
|
||||
return Arrays.stream(buttons).filter(buttonType -> button.equals(getButton(buttonType))).findFirst();
|
||||
}
|
||||
|
||||
private String formatLines(String input, int maxLength) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
BreakIterator boundary = BreakIterator.getLineInstance(Locale.ROOT);
|
||||
boundary.setText(input);
|
||||
int start = boundary.first();
|
||||
int end = boundary.next();
|
||||
int lineLength = 0;
|
||||
|
||||
while(end != BreakIterator.DONE) {
|
||||
String word = input.substring(start,end);
|
||||
lineLength = lineLength + word.length();
|
||||
if (lineLength >= maxLength) {
|
||||
builder.append("\n");
|
||||
lineLength = word.length();
|
||||
}
|
||||
builder.append(word);
|
||||
start = end;
|
||||
end = boundary.next();
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private MessageDialogButton getButton(ButtonType buttonType) {
|
||||
if(ButtonType.OK.equals(buttonType)) {
|
||||
return MessageDialogButton.OK;
|
||||
}
|
||||
if(ButtonType.CANCEL.equals(buttonType)) {
|
||||
return MessageDialogButton.Cancel;
|
||||
}
|
||||
if(ButtonType.YES.equals(buttonType)) {
|
||||
return MessageDialogButton.Yes;
|
||||
}
|
||||
if(ButtonType.NO.equals(buttonType)) {
|
||||
return MessageDialogButton.No;
|
||||
}
|
||||
if(ButtonType.CLOSE.equals(buttonType)) {
|
||||
return MessageDialogButton.Close;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Cannot find button for " + buttonType);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Optional<String> requestPassphrase(String walletName, Keystore keystore) {
|
||||
if(Platform.isFxApplicationThread()) {
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
|
||||
Optional<String> optPassphrase = showPassphraseDialog(walletName, keystore);
|
||||
Platform.runLater(() -> Platform.exitNestedEventLoop(passphraseShowing, optPassphrase));
|
||||
});
|
||||
return (Optional<String>)Platform.enterNestedEventLoop(passphraseShowing);
|
||||
} else {
|
||||
return showPassphraseDialog(walletName, keystore);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<String> showPassphraseDialog(String walletName, Keystore keystore) {
|
||||
TextInputDialogBuilder builder = new TextInputDialogBuilder().setTitle("Passphrase for " + walletName);
|
||||
builder.setDescription("Enter the BIP39 passphrase for keystore:\n" + keystore.getLabel());
|
||||
builder.setPasswordInput(true);
|
||||
String passphrase = builder.build().showDialog(SparrowTerminal.get().getGui());
|
||||
return passphrase == null ? Optional.empty() : Optional.of(passphrase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.sparrowwallet.sparrow.terminal.preferences;
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize;
|
||||
import com.googlecode.lanterna.gui2.*;
|
||||
import com.googlecode.lanterna.gui2.dialogs.DirectoryDialogBuilder;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Server;
|
||||
import com.sparrowwallet.sparrow.net.CoreAuthType;
|
||||
import com.sparrowwallet.sparrow.net.Protocol;
|
||||
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
public class BitcoinCoreDialog extends ServerUrlDialog {
|
||||
private final ComboBox<String> authentication;
|
||||
private final Label dataFolderLabel;
|
||||
private final TextBox dataFolder;
|
||||
private final Button selectDataFolder;
|
||||
private final Label userPassLabel;
|
||||
private final TextBox user;
|
||||
private final TextBox pass;
|
||||
|
||||
public BitcoinCoreDialog() {
|
||||
super("Bitcoin Core");
|
||||
|
||||
setHints(List.of(Hint.CENTERED));
|
||||
Panel mainPanel = new Panel(new GridLayout(3).setHorizontalSpacing(2).setVerticalSpacing(0));
|
||||
|
||||
if(Config.get().getCoreServer() == null) {
|
||||
Config.get().setCoreServer(new Server("http://127.0.0.1:" + Network.get().getDefaultPort()));
|
||||
}
|
||||
addUrlComponents(mainPanel, Config.get().getRecentCoreServers(), Config.get().getCoreServer());
|
||||
addLine(mainPanel);
|
||||
|
||||
mainPanel.addComponent(new Label("Authentication"));
|
||||
authentication = new ComboBox<>("Default", "User/Pass");
|
||||
authentication.setSelectedIndex(Config.get().getCoreAuthType() == CoreAuthType.USERPASS ? 0 : 1);
|
||||
mainPanel.addComponent(authentication);
|
||||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||
|
||||
dataFolderLabel = new Label("Data Folder");
|
||||
mainPanel.addComponent(dataFolderLabel);
|
||||
dataFolder = new TextBox(new TerminalSize(30, 1), Config.get().getCoreDataDir() != null ? Config.get().getCoreDataDir().getAbsolutePath() : "");
|
||||
mainPanel.addComponent(dataFolder);
|
||||
selectDataFolder = new Button("Select...");
|
||||
mainPanel.addComponent(selectDataFolder);
|
||||
|
||||
userPassLabel = new Label("User/Pass");
|
||||
mainPanel.addComponent(userPassLabel);
|
||||
user = new TextBox(new TerminalSize(16, 1));
|
||||
mainPanel.addComponent(user);
|
||||
pass = new TextBox(new TerminalSize(16, 1));
|
||||
pass.setMask('*');
|
||||
mainPanel.addComponent(pass);
|
||||
|
||||
if(Config.get().getCoreAuth() != null) {
|
||||
String[] userPass = Config.get().getCoreAuth().split(":");
|
||||
if(userPass.length > 0) {
|
||||
user.setText(userPass[0]);
|
||||
}
|
||||
if(userPass.length > 1) {
|
||||
pass.setText(userPass[1]);
|
||||
}
|
||||
}
|
||||
|
||||
authentication.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||
dataFolderLabel.setVisible(selectedIndex == 0);
|
||||
dataFolder.setVisible(selectedIndex == 0);
|
||||
selectDataFolder.setVisible(selectedIndex == 0);
|
||||
userPassLabel.setVisible(selectedIndex == 1);
|
||||
user.setVisible(selectedIndex == 1);
|
||||
pass.setVisible(selectedIndex == 1);
|
||||
Config.get().setCoreAuthType(selectedIndex == 0 ? CoreAuthType.COOKIE : CoreAuthType.USERPASS);
|
||||
});
|
||||
authentication.setSelectedIndex(Config.get().getCoreAuthType() == CoreAuthType.USERPASS ? 1 : 0);
|
||||
|
||||
dataFolder.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||
File dataDir = new File(newText);
|
||||
if(dataDir.exists()) {
|
||||
Config.get().setCoreDataDir(dataDir);
|
||||
}
|
||||
});
|
||||
selectDataFolder.addListener(button -> {
|
||||
DirectoryDialogBuilder builder = new DirectoryDialogBuilder().setTitle("Select Bitcoin Core Data Folder").setActionLabel("Select");
|
||||
builder.setShowHiddenDirectories(true);
|
||||
builder.setSelectedDirectory(Config.get().getCoreDataDir() == null ? new File(System.getProperty("user.home")) : Config.get().getCoreDataDir());
|
||||
File file = builder.build().showDialog(SparrowTerminal.get().getGui());
|
||||
if(file != null) {
|
||||
dataFolder.setText(file.getAbsolutePath());
|
||||
}
|
||||
});
|
||||
user.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||
setCoreAuth();
|
||||
});
|
||||
pass.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||
setCoreAuth();
|
||||
});
|
||||
|
||||
addLine(mainPanel);
|
||||
addProxyComponents(mainPanel);
|
||||
|
||||
Panel buttonPanel = new Panel();
|
||||
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||
buttonPanel.addComponent(new Button("Test", this::onTest));
|
||||
buttonPanel.addComponent(new Button("Done", this::onDone));
|
||||
|
||||
addLine(mainPanel);
|
||||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||
|
||||
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||
setComponent(mainPanel);
|
||||
}
|
||||
|
||||
protected void setServerConfig() {
|
||||
Server currentServer = getCurrentServer();
|
||||
if(currentServer != null) {
|
||||
Config.get().setCoreServer(currentServer);
|
||||
}
|
||||
}
|
||||
|
||||
protected void setServerAlias(Server server) {
|
||||
Config.get().setCoreServerAlias(server);
|
||||
}
|
||||
|
||||
protected Protocol getProtocol() {
|
||||
return Protocol.HTTP;
|
||||
}
|
||||
|
||||
protected void setProtocol(Protocol protocol) {
|
||||
//empty
|
||||
}
|
||||
|
||||
private void setCoreAuth() {
|
||||
Config.get().setCoreAuth(user.getText() + ":" + pass.getText());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user