Compare commits

..

No commits in common. "master" and "2.4.0" have entirely different histories.

192 changed files with 1800 additions and 4656 deletions

View File

@ -1,6 +1,6 @@
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.1.0'
id 'org-openjfx-javafxplugin'
id 'org.beryx.jlink' version '3.2.1'
id 'org.gradlex.extra-java-module-info' version '1.13.1'
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.21.0'
@ -20,7 +20,7 @@ if(System.getProperty("os.arch") == "aarch64") {
def headless = "true".equals(System.getProperty("java.awt.headless"))
group = 'com.sparrowwallet'
version = '2.5.3'
version = '2.4.0'
repositories {
mavenCentral()
@ -32,7 +32,7 @@ tasks.withType(AbstractArchiveTask).configureEach {
}
javafx {
version = "26"
version = headless ? "18" : "25.0.2"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
@ -45,24 +45,24 @@ dependencies {
implementation(project(':drongo'))
implementation(project(':lark'))
implementation('com.google.guava:guava:33.5.0-jre')
implementation('com.google.code.gson:gson:2.13.2')
implementation('com.google.code.gson:gson:2.9.1')
implementation('com.h2database:h2:2.1.214')
implementation('com.zaxxer:HikariCP:7.0.2') {
implementation('com.zaxxer:HikariCP:4.0.3') {
exclude group: 'org.slf4j'
}
implementation('org.jdbi:jdbi3-core:3.51.0') {
implementation('org.jdbi:jdbi3-core:3.49.5') {
exclude group: 'org.slf4j'
}
implementation('org.jdbi:jdbi3-sqlobject:3.51.0') {
implementation('org.jdbi:jdbi3-sqlobject:3.49.5') {
exclude group: 'org.slf4j'
}
implementation('org.flywaydb:flyway-core:9.22.3')
implementation('org.fxmisc.richtext:richtextfx:0.11.7')
implementation('no.tornado:tornadofx-controls:1.0.4')
implementation('com.google.zxing:javase:3.5.4') {
implementation('com.google.zxing:javase:3.4.0') {
exclude group: 'com.beust', module: 'jcommander'
}
implementation('org.jcommander:jcommander:3.0')
implementation('org.jcommander:jcommander:2.0')
implementation('com.github.arteam:simple-json-rpc-core:1.3')
implementation('com.github.arteam:simple-json-rpc-client:1.3') {
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
@ -70,19 +70,19 @@ dependencies {
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
exclude group: 'org.slf4j'
}
implementation('com.fasterxml.jackson.core:jackson-databind:2.21.1')
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
implementation('com.sparrowwallet:hummingbird:1.7.4')
implementation('co.nstant.in:cbor:0.9')
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
implementation("io.matthewnelson.kmp-tor:runtime:2.5.0")
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.21.0")
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.2') {
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.1') {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
}
implementation('de.jangassen:nsmenufx:3.1.0') {
exclude group: 'net.java.dev.jna', module: 'jna'
}
implementation('org.controlsfx:controlsfx:11.2.3' ) {
implementation('org.controlsfx:controlsfx:11.1.0' ) {
exclude group: 'org.openjfx', module: 'javafx-base'
exclude group: 'org.openjfx', module: 'javafx-graphics'
exclude group: 'org.openjfx', module: 'javafx-controls'
@ -93,25 +93,26 @@ dependencies {
}
implementation('dev.bwt:bwt-jni:0.1.8')
implementation('net.sourceforge.javacsv:javacsv:2.0')
implementation ('org.slf4j:slf4j-api:2.0.17')
implementation('org.slf4j:jul-to-slf4j:2.0.17') {
implementation ('org.slf4j:slf4j-api:2.0.12')
implementation('org.slf4j:jul-to-slf4j:2.0.12') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
implementation('com.sparrowwallet:tern:1.0.6')
implementation('io.reactivex.rxjava2:rxjava:2.2.21')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.20.0')
implementation('org.apache.commons:commons-compress:1.28.0')
implementation('com.github.librepdf:openpdf:1.3.43')
implementation('com.googlecode.lanterna:lanterna:3.1.5')
implementation('net.coobird:thumbnailator:0.4.21')
implementation('org.apache.commons:commons-lang3:3.19.0')
implementation('org.apache.commons:commons-compress:1.27.1')
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
implementation('com.github.librepdf:openpdf:1.3.30')
implementation('com.googlecode.lanterna:lanterna:3.1.3')
implementation('net.coobird:thumbnailator:0.4.18')
implementation('com.github.hervegirod:fxsvgimage:1.1')
implementation('com.sparrowwallet:toucan:0.9.0')
implementation('com.jcraft:jzlib:1.1.3')
implementation('io.github.doblon8:jzbar:0.4.0')
testImplementation('org.junit.jupiter:junit-jupiter-api:5.14.1')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.14.1')
implementation('io.github.doblon8:jzbar:0.3.0')
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
}
@ -124,6 +125,14 @@ compileJava {
}
}
processResources {
doLast {
delete fileTree("$buildDir/resources/main/native").matching {
exclude "${osName}/${osArch}/**"
}
}
}
test {
useJUnitPlatform()
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--enable-native-access=ALL-UNNAMED"]
@ -162,7 +171,7 @@ application {
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"]
}
if(headless) {
applicationDefaultJvmArgs += ["-Dglass.platform=Headless"]
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
}
}
@ -180,34 +189,7 @@ jlink {
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
}
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6',
'--no-header-files', '--no-man-pages', '--ignore-signing-information',
'--exclude-files', '**.png',
'--exclude-resources',
'glob:/com.sparrowwallet.merged.module/META-INF/*,' +
'glob:/javafx.graphics/*.dylib,' +
'glob:/javafx.graphics/*.so,' +
'glob:/javafx.graphics/*.dll,' +
'glob:/com.sparrowwallet.drongo/native/**,' +
'glob:/com.sparrowwallet.sparrow/native/**,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.so,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.dylib,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.jnilib,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.dll,' +
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.a,' +
'glob:/com.sparrowwallet.merged.module/darwin-*/**,' +
'glob:/com.sparrowwallet.merged.module/linux-*/**,' +
'glob:/com.sparrowwallet.merged.module/win32-*/**,' +
'glob:/org.usb4java/org/usb4java/darwin-*/**,' +
'glob:/org.usb4java/org/usb4java/linux-*/**,' +
'glob:/org.usb4java/org/usb4java/win32-*/**,' +
'glob:/org.hid4java/darwin-*/**,' +
'glob:/org.hid4java/linux-*/**,' +
'glob:/org.hid4java/win32-*/**,' +
'glob:/openpnp.capture.java/darwin-*/**,' +
'glob:/openpnp.capture.java/linux-*/**,' +
'glob:/openpnp.capture.java/win32-*/**,' +
'glob:/io.github.doblon8.jzbar/native/**']
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--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 = ["--enable-native-access=com.sparrowwallet.drongo",
@ -217,7 +199,6 @@ jlink {
"--enable-native-access=com.fazecast.jSerialComm",
"--enable-native-access=org.usb4java",
"--enable-native-access=io.github.doblon8.jzbar",
"--enable-native-access=com.sparrowwallet.sparrow",
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
@ -256,7 +237,7 @@ jlink {
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
}
if(headless) {
jvmArgs += ["-Dglass.platform=Headless"]
jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
}
}
addExtraDependencies("javafx")
@ -297,13 +278,13 @@ jlink {
}
if(os.linux) {
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules', 'extractNativeLibraries')
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
tasks.jpackageImage.finalizedBy('prepareResourceDir')
if(!headless) {
tasks.jpackage.dependsOn('copyMimeInfo')
}
} else {
tasks.jlink.finalizedBy('addUserWritePermission', 'extractNativeLibraries')
tasks.jlink.finalizedBy('addUserWritePermission')
}
tasks.register('addUserWritePermission', Exec) {
@ -382,74 +363,6 @@ tasks.register('packageTarDistribution', Tar) {
}
}
def jnaPlatform
if(os.macOsX) {
jnaPlatform = "darwin-${osArch == 'aarch64' ? 'aarch64' : 'x86-64'}"
} else if(os.windows) {
jnaPlatform = "win32-x86-64"
} else {
jnaPlatform = "linux-${osArch == 'aarch64' ? 'aarch64' : 'x86-64'}"
}
def serialOs = os.macOsX ? "OSX" : (os.windows ? "Windows" : "Linux")
def serialArch = osArch == "aarch64" ? "aarch64" : "x86_64"
// Map of JAR name prefix to the include glob for platform-specific natives inside the JAR.
def nativeLibJars = [
'jna-' : "com/sun/jna/${jnaPlatform}/*",
'argon2-jvm-2' : "${jnaPlatform}/*",
'hid4java-' : "${jnaPlatform}/*",
'openpnp-capture-java': "${jnaPlatform}/*",
'jSerialComm-' : "${serialOs}/${serialArch}/*",
'usb4java-' : "org/usb4java/${jnaPlatform}/*",
'jzbar-' : "native/${osName}/${osArch}/*",
]
tasks.register('extractNativeLibraries') {
dependsOn 'jlink'
doLast {
def imageLib = file("$buildDir/image/lib")
// Project-owned natives
copy {
from "${project(':drongo').projectDir}/src/main/resources/native/${osName}/${osArch}", "src/main/resources/native/${osName}/${osArch}"
into imageLib
eachFile { it.permissions { unix('rw-r--r--') } }
}
// JavaFX natives
def javafxClassifier = ""
if(os.macOsX) {
javafxClassifier = osArch == "aarch64" ? "mac-aarch64" : "mac"
} else if(os.windows) {
javafxClassifier = "win"
} else {
javafxClassifier = osArch == "aarch64" ? "linux-aarch64" : "linux"
}
def javafxJar = configurations.runtimeClasspath.find { it.name == "javafx-graphics-${javafx.version}-${javafxClassifier}.jar" }
if(javafxJar) {
copy {
from(zipTree(javafxJar)) { include "*.dylib", "*.so", "*.dll" }
into imageLib
eachFile { it.permissions { unix('rw-r--r--') } }
}
}
// Third-party natives
nativeLibJars.each { prefix, includePattern ->
def jar = configurations.runtimeClasspath.find { it.name.startsWith(prefix) }
if(jar) {
copy {
from(zipTree(jar)) { include includePattern }
into imageLib
eachFile { it.path = it.name; it.permissions { unix('rw-r--r--') } }
includeEmptyDirs = false
}
}
}
}
}
extraJavaModuleInfo {
module('no.tornado:tornadofx-controls', 'tornadofx.controls') {
exports('tornadofx.control')
@ -549,6 +462,16 @@ extraJavaModuleInfo {
exports('co.nstant.in.cbor.model')
exports('co.nstant.in.cbor.builder')
}
module('net.sourceforge.streamsupport:streamsupport', 'net.sourceforge.streamsupport') {
requires('jdk.unsupported')
exports('java8.util')
exports('java8.util.function')
exports('java8.util.stream')
}
module('net.coobird:thumbnailator', 'net.coobird.thumbnailator') {
exports('net.coobird.thumbnailator')
requires('java.desktop')
}
module('org.jcommander:jcommander', 'org.jcommander') {
exports('com.beust.jcommander')
}

23
buildSrc/build.gradle Normal file
View File

@ -0,0 +1,23 @@
plugins {
id 'java-gradle-plugin' // so we can assign and ID to our plugin
}
dependencies {
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
}
repositories {
mavenCentral()
maven {
url = uri("https://plugins.gradle.org/m2/")
}
}
gradlePlugin {
plugins {
register("org-openjfx-javafxplugin") {
id = "org-openjfx-javafxplugin"
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,47 @@
/*
* 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.openjfx.gradle.tasks.ExecTask;
public class JavaFXPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPlugins().apply(OsDetectorPlugin.class);
project.getExtensions().create("javafx", JavaFXOptions.class, project);
project.getTasks().register("configJavafxRun", ExecTask.class, project);
}
}

View File

@ -0,0 +1,90 @@
/*
* 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.plugins.ApplicationPlugin;
import org.gradle.api.tasks.JavaExec;
import org.gradle.api.tasks.TaskAction;
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.Arrays;
import java.util.TreeSet;
public class ExecTask extends DefaultTask {
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()) {
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()));
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
}
} else {
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
}
}
private static boolean isJavaFXJar(File jar, JavaFXPlatform platform) {
return jar.isFile() &&
Arrays.stream(JavaFXModule.values()).anyMatch(javaFXModule ->
javaFXModule.compareJarFileName(platform, jar.getName()) ||
javaFXModule.getModuleJarFileName().equals(jar.getName()));
}
}

View File

@ -12,36 +12,63 @@ Work on resolving both of these issues is ongoing.
### 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.6 to v1.9.1, this was Eclipse Temurin 18.0.1+10. For v2.0.0 to v2.3.1, this was Eclipse Temurin 22.0.2+9. For v2.4.0 and later, Eclipse Temurin 25.0.2+10 is used.
Note: Do not install Java using a system package manager (e.g. apt, dnf, rpm).
Linux packages replace the JDK's bundled `cacerts` file with a symlink to the system CA certificates, which differ from those in the release tarballs and will produce a non-reproducible build.
For v1.6.6 to v1.9.1, this was Eclipse Temurin 18.0.1+10. For v2.0.0 and later, Eclipse Temurin 22.0.2+9 is used.
#### Java from Adoptium github repo
It is available for all supported platforms from [Eclipse Temurin 25.0.2+10](https://github.com/adoptium/temurin25-binaries/releases/tag/jdk-25.0.2%2B10).
It is available for all supported platforms from [Eclipse Temurin 22.0.2+9](https://github.com/adoptium/temurin22-binaries/releases/tag/jdk-22.0.2%2B9).
For reference, the downloads are as follows:
- [Linux x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz)
- [Linux aarch64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_linux_hotspot_25.0.2_10.tar.gz)
- [MacOS x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_mac_hotspot_25.0.2_10.tar.gz)
- [MacOS aarch64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_mac_hotspot_25.0.2_10.tar.gz)
- [Windows x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_windows_hotspot_25.0.2_10.zip)
- [Linux x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_linux_hotspot_22.0.2_9.tar.gz)
- [Linux aarch64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_linux_hotspot_22.0.2_9.tar.gz)
- [MacOS x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_mac_hotspot_22.0.2_9.tar.gz)
- [MacOS aarch64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_mac_hotspot_22.0.2_9.tar.gz)
- [Windows x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_windows_hotspot_22.0.2_9.zip)
On Linux, extract the tarball and set `JAVA_HOME` to use it for the build:
```shell
tar -xzf OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz
export JAVA_HOME=$PWD/jdk-25.0.2+10
export PATH=$JAVA_HOME/bin:$PATH
#### Java from Adoptium deb repo
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
- Install dependencies:
```sh
sudo apt-get install -y wget curl apt-transport-https gnupg
```
#### Java from SDKMAN
Download Adoptium public PGP key:
```sh
curl --tlsv1.2 --proto =https --location -o adoptium.asc https://packages.adoptium.net/artifactory/api/gpg/key/public
```
An 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).
Check if key fingerprint matches: `3B04D753C9050D9A5D343F39843C48A565F8F04B`:
```
gpg --import --import-options show-only adoptium.asc
```
If key doesn't match, do not proceed.
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-22-jdk=22.0.2+9
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 25.0.2-tem
sdk install java 22.0.2-tem
```
### Other requirements
@ -56,7 +83,7 @@ sudo apt install -y rpm fakeroot binutils
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
```shell
GIT_TAG="2.5.2"
GIT_TAG="2.3.1"
```
The project can then be initially cloned as follows:
@ -74,7 +101,7 @@ git checkout "${GIT_TAG}"
```
Note - there is an additional step if you updated rather than initially cloned your repo at `GIT_TAG`.
This is due to the Git submodules which need to be checked out to the commit state they had at the time of the release.
This is due to the [drongo submodule](https://github.com/sparrowwallet/drongo/tree/master) which needs to be checked out to the commit state it had at the time of the release.
Only then your build will be comparable to the provided one in the release section of Github.
To checkout the submodule to the correct commit for `GIT_TAG`, additionally run:

2
drongo

@ -1 +1 @@
Subproject commit 077d2142cc3aad84f6f58868cf8f17fc61027fdc
Subproject commit 0a500ea00253327207e36d6a56c7b7471e4c891f

2
lark

@ -1 +1 @@
Subproject commit e9c6f35fe66aee105ef3c532fcefeb7130dab169
Subproject commit 36cf0c85dc299ec7299abd0abc3453b5036d941a

View File

@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2.5.3</string>
<string>2.4.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->

View File

@ -88,6 +88,7 @@ public class AppController implements Initializable {
public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
public static final String CONNECTION_FAILED_PREFIX = "Connection failed: ";
public static final String TRYING_ANOTHER_SERVER_MESSAGE = "trying another server...";
public static final String JPACKAGE_APP_PATH = "jpackage.app-path";
@FXML
private VBox rootBox;
@ -419,7 +420,7 @@ public class AppController implements Initializable {
networkItem.setOnAction(event -> restart(event, network));
restart.getItems().add(networkItem);
}
restart.setVisible(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH) != null);
restart.setVisible(System.getProperty(JPACKAGE_APP_PATH) != null);
saveTransaction.setDisable(true);
showTransaction.visibleProperty().bind(Bindings.and(saveTransaction.visibleProperty(), saveTransaction.disableProperty().not()));
@ -471,7 +472,7 @@ public class AppController implements Initializable {
private void setPlatformApplicationMenu() {
OsType osType = OsType.getCurrent();
if(osType == OsType.MACOS && Interface.get() == Interface.DESKTOP) {
if(osType == OsType.MACOS) {
MenuToolkit tk = MenuToolkit.toolkit();
MenuItem settings = new MenuItem("Settings...");
settings.setOnAction(this::openSettings);
@ -596,7 +597,7 @@ public class AppController implements Initializable {
sudo groupadd -f -r plugdev
sudo usermod -aG plugdev `whoami`
""";
String home = System.getProperty(SparrowWallet.JPACKAGE_APP_PATH);
String home = System.getProperty(JPACKAGE_APP_PATH);
if(home != null && !home.startsWith("/opt/sparrowwallet") && home.endsWith("bin/Sparrow")) {
home = home.replace("bin/Sparrow", "");
commands = commands.replace("/opt/sparrowwallet/", home);
@ -1044,8 +1045,8 @@ public class AppController implements Initializable {
}
public void restart(ActionEvent event, Network network) {
if(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH) == null) {
throw new IllegalStateException("Property " + SparrowWallet.JPACKAGE_APP_PATH + " is not present");
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
}
Args args = getRestartArgs();
@ -1066,7 +1067,7 @@ public class AppController implements Initializable {
private void restart(ActionEvent event, Args args) {
try {
List<String> cmd = new ArrayList<>();
cmd.add(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH));
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
cmd.addAll(args.toParams());
final ProcessBuilder builder = new ProcessBuilder(cmd);
if(OsType.getCurrent() == OsType.UNIX) {
@ -1121,7 +1122,7 @@ public class AppController implements Initializable {
WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get();
File walletFile = Storage.getWalletFile(nameAndBirthDate.getName());
Storage storage = new Storage(walletFile);
Wallet wallet = new Wallet(nameAndBirthDate.getName(), PolicyType.SINGLE_HD, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate());
Wallet wallet = new Wallet(nameAndBirthDate.getName(), PolicyType.SINGLE, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate());
addWalletTabOrWindow(storage, wallet, false);
}
}
@ -1139,37 +1140,12 @@ public class AppController implements Initializable {
AppServices.moveToActiveWindowScreen(window, 800, 450);
List<File> files = fileChooser.showOpenMultipleDialog(window);
if(files != null) {
configureWalletsDir(files);
for(File file : files) {
openWalletFile(file, forceSameWindow);
}
}
}
private static void configureWalletsDir(List<File> files) {
List<File> parentDirs = files.stream().map(File::getParentFile).distinct().collect(Collectors.toList());
if(parentDirs.size() == 1 && !Boolean.FALSE.equals(Config.get().getSuggestChangeWalletsDir())) {
File selectedDir = parentDirs.getFirst();
boolean sameDir;
try {
sameDir = Files.isSameFile(selectedDir.toPath(), Storage.getWalletsDir().toPath());
} catch(IOException e) {
sameDir = selectedDir.toPath().normalize().equals(Storage.getWalletsDir().toPath().normalize());
}
if(!sameDir) {
ConfirmationAlert alert = new ConfirmationAlert("Change wallets directory?",
"Do you want to configure Sparrow to use " + selectedDir + " as the default wallets directory?", ButtonType.NO, ButtonType.YES);
Optional<ButtonType> optType = alert.showAndWait();
if(optType.isPresent() && optType.get() == ButtonType.YES) {
Config.get().setWalletsDir(selectedDir);
Config.get().setSuggestChangeWalletsDir(null);
} else if(alert.isDontAskAgain()) {
Config.get().setSuggestChangeWalletsDir(Boolean.FALSE);
}
}
}
}
public void openWalletFile(File file, boolean forceSameWindow) {
try {
Storage storage = new Storage(file);
@ -1271,7 +1247,7 @@ public class AppController implements Initializable {
List<ExtendedKey> xpubs = wallet.getKeystores().stream().map(Keystore::getExtendedPublicKey).collect(Collectors.toList());
Optional<WalletForm> optNewWalletForm = walletTabData.stream()
.map(WalletTabData::getWalletForm)
.filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI_HD &&
.filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI &&
wf.getSettingsWalletForm().getWallet().getScriptType() == wallet.getScriptType() && !wf.getSettingsWalletForm().getWallet().isValid() &&
wf.getSettingsWalletForm().getWallet().getKeystores().stream().map(Keystore::getExtendedPublicKey).anyMatch(xpubs::contains)).findFirst();
if(optNewWalletForm.isPresent()) {
@ -1286,7 +1262,6 @@ public class AppController implements Initializable {
private boolean attemptImportWallet(File file, SecureString password) {
List<WalletImport> walletImporters = List.of(new ColdcardSinglesig(), new ColdcardMultisig(),
new Bip129(),
new Electrum(),
new SpecterDesktop(),
new Descriptor(),
@ -1328,16 +1303,13 @@ public class AppController implements Initializable {
return;
}
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getPolicyType(), wallet.getBirthDate(), false);
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
nameDlg.initOwner(rootStack.getScene().getWindow());
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
if(optNameAndBirthDate.isPresent()) {
WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get();
wallet.setName(nameAndBirthDate.getName());
wallet.setBirthDate(nameAndBirthDate.getBirthDate());
if(wallet.getPolicyType() == PolicyType.SINGLE_SP && wallet.getBirthDate() == null) {
wallet.setBirthDate(new Date());
}
} else {
return;
}
@ -1476,7 +1448,7 @@ public class AppController implements Initializable {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
Wallet wallet = selectedWalletForm.getWallet();
if(wallet.getPolicyType() == PolicyType.SINGLE_HD || wallet.getPolicyType() == PolicyType.SINGLE_SP) {
if(wallet.getKeystores().size() == 1) {
//Can sign and verify
messageSignDialog = new MessageSignDialog(wallet);
}
@ -1511,7 +1483,7 @@ public class AppController implements Initializable {
bitcoinUnit = wallet.getAutoUnit();
}
sendToManyDialog = new SendToManyDialog(bitcoinUnit, Config.get().getUnitFormat(), initialPayments);
sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
sendToManyDialog.initModality(Modality.NONE);
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
sendToManyDialog = null;
@ -2058,34 +2030,6 @@ public class AppController implements Initializable {
AppServices.showErrorDialog("Invalid PSBT", e.getMessage());
return;
}
try {
psbt.verifySigHashes();
} catch(PSBTSignatureException e) {
Optional<ButtonType> result = AppServices.showWarningDialog("Non-Default Sighash",
e.getMessage() + "\n\nReview this PSBT carefully before signing.\n\nOpen the transaction?", ButtonType.YES, ButtonType.NO);
if(result.isEmpty() || result.get() != ButtonType.YES) {
return;
}
}
}
//Skip the warning for already-confirmed transactions loaded for inspection
if(blockTransaction == null) {
List<TransactionOutput> unknownScriptOutputs = new ArrayList<>();
for(int i = 0; i < transaction.getOutputs().size(); i++) {
TransactionOutput txOutput = transaction.getOutputs().get(i);
if(txOutput.getValue() > 0 && txOutput.getScript().getToAddress() == null) {
//Silent payment outputs have an empty script and non-zero value until the recipient script is computed
if(psbt != null && i < psbt.getPsbtOutputs().size() && psbt.getPsbtOutputs().get(i).getSilentPaymentAddress() != null) {
continue;
}
unknownScriptOutputs.add(txOutput);
}
}
if(!unknownScriptOutputs.isEmpty() && !confirmUnknownScriptOutputs(unknownScriptOutputs)) {
return;
}
}
try {
@ -2192,23 +2136,6 @@ public class AppController implements Initializable {
return result.isPresent() && result.get() == ButtonType.YES;
}
private boolean confirmUnknownScriptOutputs(List<TransactionOutput> unknownScriptOutputs) {
long totalAmount = unknownScriptOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
BitcoinUnit unit = Config.get().getBitcoinUnit();
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
unit = totalAmount >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS;
}
String amount = unit.equals(BitcoinUnit.BTC) ? format.formatBtcValue(totalAmount) + " BTC" : format.formatSatsValue(totalAmount) + " sats";
String outputDesc = unknownScriptOutputs.size() == 1 ? "an output" : unknownScriptOutputs.size() + " outputs";
Optional<ButtonType> result = AppServices.showWarningDialog("Unknown Script Type",
"This transaction contains " + outputDesc + " of a non-standard or unrecognised script type, totalling " + amount + ".\n\n" +
"Sparrow cannot resolve these outputs to addresses, so they will not appear in the transaction diagram. " +
"Review the individual output(s) in the transaction tree carefully before signing or broadcasting.\n\n" +
"Open the transaction?", ButtonType.YES, ButtonType.NO);
return result.isPresent() && result.get() == ButtonType.YES;
}
private String getTabName(Tab tab) {
return ((Label)tab.getGraphic()).getText();
}

View File

@ -69,11 +69,9 @@ import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppController.CONNECTION_FAILED_PREFIX;
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
public class AppServices {
@ -330,9 +328,6 @@ public class AppServices {
"\n\nChange the configured server certificate if you would like to proceed.");
} else {
crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
if(crtFile == null) {
crtFile = Storage.getCaCertificateFile(tlsServerException.getServer().getHost());
}
if(crtFile != null) {
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
"\n\nThis may be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." +
@ -369,18 +364,15 @@ public class AppServices {
onlineProperty.setValue(false);
onlineProperty.addListener(onlineServicesListener);
log.debug("Connection failed", failEvent.getSource().getException());
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
boolean changed = changePublicServer();
connectionService.setPeriod(changed ? Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS) : Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
if(!changed) {
Platform.runLater(() -> EventManager.get().post(new StatusEvent(CONNECTION_FAILED_PREFIX + "No public servers available that can serve the open wallets, retrying later...")));
}
Config.get().changePublicServer();
connectionService.setPeriod(Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS));
} else {
connectionService.setPeriod(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
}
log.debug("Connection failed", failEvent.getSource().getException());
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
});
return connectionService;
@ -856,10 +848,6 @@ public class AppServices {
public static void clearTransactionHistoryCache(Wallet wallet) {
ElectrumServer.clearRetrievedScriptHashes(wallet);
if(wallet.getPolicyType() == PolicyType.SINGLE_SP && wallet.isValid()) {
ElectrumServer.releaseSilentPaymentSubscription(wallet.getSilentPaymentScanAddress());
}
for(Wallet childWallet : wallet.getChildWallets()) {
if(childWallet.isNested()) {
AppServices.clearTransactionHistoryCache(childWallet);
@ -871,22 +859,6 @@ public class AppServices {
return Storage.isWalletFile(file);
}
public boolean changePublicServer() {
List<PolicyType> policyTypes = getOpenWallets().keySet().stream().map(Wallet::getPolicyType).filter(Objects::nonNull).collect(Collectors.toList());
return changePublicServer(policyTypes.isEmpty() ? List.of(PolicyType.SINGLE_HD) : policyTypes);
}
private boolean changePublicServer(List<PolicyType> policyTypes) {
Config config = Config.get();
List<Server> otherServers = PublicElectrumServer.getServers().stream().filter(pes -> pes.supportsAllPolicyTypes(policyTypes))
.map(PublicElectrumServer::getServer).filter(server -> !server.equals(config.getPublicElectrumServer())).collect(Collectors.toList());
if(!otherServers.isEmpty()) {
config.setPublicElectrumServer(otherServers.get(ThreadLocalRandom.current().nextInt(otherServers.size())));
return true;
}
return false;
}
public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons);
}
@ -1102,7 +1074,7 @@ public class AppServices {
try {
Auth47 auth47 = new Auth47(uri);
List<ScriptType> scriptTypes = PaymentCode.SEGWIT_SCRIPT_TYPES;
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE_HD), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
if(wallet != null) {
try {
@ -1122,8 +1094,8 @@ public class AppServices {
private static void openLnurlAuthUri(URI uri) {
try {
LnurlAuth lnurlAuth = new LnurlAuth(uri);
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD);
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE_HD), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
if(wallet != null) {
if(wallet.isEncrypted()) {
@ -1237,7 +1209,6 @@ public class AppServices {
public static boolean isWhirlpoolCompatible(Wallet wallet) {
return WHIRLPOOL_NETWORKS.contains(Network.get())
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
&& wallet.getKeystores().size() == 1
&& wallet.getKeystores().get(0).hasSeed()
@ -1248,7 +1219,6 @@ public class AppServices {
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
return WHIRLPOOL_NETWORKS.contains(Network.get())
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
&& wallet.getKeystores().size() == 1
&& wallet.getKeystores().getFirst().getWalletModel() != WalletModel.BITBOX_02; //BitBox02 does not support high account numbers
@ -1484,28 +1454,10 @@ public class AppServices {
@Subscribe
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
String currentName = Config.get().getServerDisplayName();
onlineProperty.set(false);
boolean changed = changePublicServer();
if(changed) {
log.warn("Failed to fetch wallet history from " + currentName + ", reconnecting to another server...");
} else {
log.warn("Failed to fetch wallet history from " + currentName + ", retrying later");
connectionService.setDelay(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
EventManager.get().post(new StatusEvent("Wallet load failed: No other public servers available that can serve the open wallets, retrying later..."));
}
log.warn("Failed to fetch wallet history from " + Config.get().getServerDisplayName() + ", reconnecting to another server...");
Config.get().changePublicServer();
onlineProperty.set(true);
}
}
@Subscribe
public void silentPaymentsUnsubscribe(SilentPaymentsUnsubscribeEvent event) {
if(isConnected()) {
ElectrumServer.SilentPaymentsUnsubscribeService unsubscribeService = new ElectrumServer.SilentPaymentsUnsubscribeService(event.getScanAddress());
unsubscribeService.setOnFailed(workerStateEvent -> {
log.warn("Failed to unsubscribe silent payments for " + event.getScanAddress().getAddress(), workerStateEvent.getSource().getException());
});
unsubscribeService.start();
}
}
}

View File

@ -55,7 +55,7 @@ public abstract class BaseController {
descriptorArea.setMouseOverTextDelay(Duration.ofMillis(150));
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
TwoDimensional.Position position = descriptorArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_HD || descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_SP ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
if(position.getMajor() > 0 && index >= 0 && index < descriptorArea.getWallet().getKeystores().size()) {
Keystore hoverKeystore = descriptorArea.getWallet().getKeystores().get(index);
Point2D pos = e.getScreenPosition();
@ -75,11 +75,7 @@ public abstract class BaseController {
builder.append(keystore.getKeyDerivation().getMasterFingerprint());
builder.append(KeyDerivation.writePath(KeyDerivation.parsePath(keystore.getKeyDerivation().getDerivationPath())).substring(1));
builder.append("]");
if(keystore.getExtendedPublicKey() != null) {
builder.append(keystore.getExtendedPublicKey().toString());
} else if(keystore.getSilentPaymentScanAddress() != null) {
builder.append(keystore.getSilentPaymentScanAddress().toKeyString());
}
builder.append(keystore.getExtendedPublicKey().toString());
return builder.toString();
}

View File

@ -8,13 +8,13 @@ public enum Interface {
public static Interface get() {
if(currentInterface == null) {
boolean headless = java.awt.GraphicsEnvironment.isHeadless();
boolean headlessPlatform = "Headless".equalsIgnoreCase(System.getProperty("glass.platform"));
boolean monocle = "Monocle".equalsIgnoreCase(System.getProperty("glass.platform"));
if(headless || headlessPlatform) {
if(headless || monocle) {
currentInterface = TERMINAL;
if(headless && !headlessPlatform) {
throw new UnsupportedOperationException("Headless environment detected but headless glass platform not found");
if(headless && !monocle) {
throw new UnsupportedOperationException("Headless environment detected but Monocle platform not found");
}
} else {
currentInterface = DESKTOP;

View File

@ -18,24 +18,14 @@ import java.util.*;
public class SparrowWallet {
public static final String APP_ID = "sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "2.5.3";
public static final String APP_VERSION = "2.4.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";
public static final String JPACKAGE_APP_PATH = "jpackage.app-path";
private static Instance instance;
public static void main(String[] argv) {
if(System.getProperty(JPACKAGE_APP_PATH) != null) {
String libDir = System.getProperty("java.home") + File.separator + "lib";
System.setProperty("jna.boot.library.path", libDir);
System.setProperty("jna.library.path", libDir);
System.setProperty("jSerialComm.library.path", libDir);
System.setProperty("org.usb4java.LibraryName", "usb4java");
System.setProperty("java.library.path", libDir);
}
Args args = new Args();
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
jCommander.parse(argv);

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
@ -69,7 +68,7 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
if(!availableAccounts.isEmpty() && (masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|| (masterWallet.getPolicyType() == PolicyType.SINGLE_HD && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
dialogPane.getButtonTypes().add(discoverButtonType);
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());

View File

@ -28,7 +28,6 @@ public class AddressLabelSkin extends LabelSkin {
super(control);
displayFlow = new TextFlow();
displayFlow.setManaged(false);
displayFlow.setMouseTransparent(true);
getChildren().addFirst(displayFlow);

View File

@ -37,7 +37,6 @@ public class AddressTextFieldSkin extends CustomTextFieldSkin {
super(control);
displayFlow = new TextFlow();
displayFlow.setManaged(false);
displayFlow.setMouseTransparent(true);
clip = new Rectangle();

View File

@ -80,7 +80,6 @@ public class AddressTreeTable extends CoinTreeTable {
contextMenu.getItems().add(showCountItem);
getColumns().forEach(col -> col.setContextMenu(contextMenu));
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
setEditable(true);
setupColumnWidths();

View File

@ -18,7 +18,6 @@ public class AddressTreeTableCellSkin<S, T> extends TreeTableCellSkin<S, T> {
super(cell);
displayFlow = new TextFlow();
displayFlow.setManaged(false);
displayFlow.setMouseTransparent(true);
displayFlow.setMinWidth(Region.USE_PREF_SIZE);
getChildren().add(displayFlow);

View File

@ -3,7 +3,6 @@ package com.sparrowwallet.sparrow.control;
import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.*;
@ -45,7 +44,6 @@ public class CardImportPane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(CardImportPane.class);
private final KeystoreCardImport importer;
private final PolicyType policyType;
private List<ChildNumber> derivation;
protected Button importButton;
private final SimpleStringProperty pin = new SimpleStringProperty("");
@ -53,7 +51,6 @@ public class CardImportPane extends TitledDescriptionPane {
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation defaultDerivation, KeyDerivation requiredDerivation) {
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel());
this.importer = importer;
this.policyType = wallet.getPolicyType();
this.derivation = requiredDerivation == null ? getDefaultDerivation(wallet, defaultDerivation) : requiredDerivation.getDerivation();
}
@ -62,7 +59,7 @@ public class CardImportPane extends TitledDescriptionPane {
return defaultDerivation.getDerivation();
}
return wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
return wallet == null || wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
}
@Override
@ -114,7 +111,7 @@ public class CardImportPane extends TitledDescriptionPane {
return;
}
CardImportService cardImportService = new CardImportService(importer, policyType, pin.get(), derivation, messageProperty);
CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty);
cardImportService.setOnSucceeded(event -> {
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
});
@ -355,14 +352,12 @@ public class CardImportPane extends TitledDescriptionPane {
public static class CardImportService extends Service<Keystore> {
private final KeystoreCardImport cardImport;
private final PolicyType policyType;
private final String pin;
private final List<ChildNumber> derivation;
private final StringProperty messageProperty;
public CardImportService(KeystoreCardImport cardImport, PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
public CardImportService(KeystoreCardImport cardImport, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
this.cardImport = cardImport;
this.policyType = policyType;
this.pin = pin;
this.derivation = derivation;
this.messageProperty = messageProperty;
@ -373,7 +368,7 @@ public class CardImportPane extends TitledDescriptionPane {
return new Task<>() {
@Override
protected Keystore call() throws Exception {
return cardImport.getKeystore(policyType, pin, derivation, messageProperty);
return cardImport.getKeystore(pin, derivation, messageProperty);
}
};
}

View File

@ -81,7 +81,7 @@ public class CodexKeystoreImportPane extends TitledDescriptionPane {
private void importKeystore(List<ChildNumber> derivation) {
importButton.setDisable(true);
try {
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, secretShareProperty.get());
Keystore keystore = importer.getKeystore(derivation, secretShareProperty.get());
EventManager.get().post(new KeystoreImportEvent(keystore));
} catch(ImportException e) {
String errorMessage = e.getMessage();
@ -125,7 +125,7 @@ public class CodexKeystoreImportPane extends TitledDescriptionPane {
private void onInputChange(boolean empty, boolean validChecksum) {
if(!empty) {
try {
importer.getKeystore(wallet.getPolicyType(), ScriptType.P2WPKH.getDefaultDerivation(), secretShareProperty.get());
importer.getKeystore(ScriptType.P2WPKH.getDefaultDerivation(), secretShareProperty.get());
validChecksum = true;
} catch(ImportException e) {
invalidLabel.setText("Invalid checksum");

View File

@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.SortDirection;
import com.sparrowwallet.drongo.wallet.TableType;
import com.sparrowwallet.drongo.wallet.Wallet;
@ -16,10 +15,7 @@ import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.BitcoindClient;
import com.sparrowwallet.sparrow.wallet.Entry;
import io.reactivex.Observable;
import io.reactivex.subjects.PublishSubject;
@ -110,7 +106,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
setPlaceholder(new Label("Error loading transactions: " + event.getErrorMessage()));
} else if(event.isLoading()) {
if(event.getStatusMessage() != null) {
setPlaceholder(new Label(event.getStatusMessage() + (event.getStatusMessage().contains("...") ? "" : "...")));
setPlaceholder(new Label(event.getStatusMessage() + "..."));
} else {
setPlaceholder(new Label("Loading transactions..."));
}
@ -126,7 +122,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
StackPane stackPane = new StackPane();
stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions"));
if((Config.get().getServerType() == ServerType.BITCOIN_CORE || wallet.getPolicyType() == PolicyType.SINGLE_SP) && !AppServices.isConnecting() && !isFullyScanned(wallet)) {
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && !AppServices.isConnecting()) {
Hyperlink hyperlink = new Hyperlink();
hyperlink.setTranslateY(30);
hyperlink.setOnAction(event -> {
@ -137,7 +133,6 @@ public class CoinTreeTable extends TreeTableView<Entry> {
Storage storage = AppServices.get().getOpenWallets().get(wallet);
Wallet pastWallet = wallet.copy();
wallet.setBirthDate(optDate.get());
wallet.setBirthHeight(null);
//Trigger background save of birthdate
EventManager.get().post(new WalletDataChangedEvent(wallet));
//Trigger full wallet rescan
@ -153,47 +148,12 @@ public class CoinTreeTable extends TreeTableView<Entry> {
}
stackPane.getChildren().add(hyperlink);
} else if(!AppServices.isConnecting() && Config.get().getServerType() == ServerType.BITCOIN_CORE && isFullyScanned(wallet)) {
Date prunedDate = getPrunedDate();
if(prunedDate != null) {
DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN);
Label prunedLabel = new Label("Scanned to pruned start date of " + dateFormat.format(prunedDate));
prunedLabel.setTranslateY(30);
stackPane.getChildren().add(prunedLabel);
}
}
stackPane.setAlignment(Pos.CENTER);
return stackPane;
}
private boolean isFullyScanned(Wallet wallet) {
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
return wallet.isValid() && ElectrumServer.isSilentPaymentsFullyCovered(wallet.getSilentPaymentScanAddress());
}
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
Date prunedDate = getPrunedDate();
return prunedDate != null && wallet.getBirthDate() != null && !wallet.getBirthDate().after(prunedDate);
}
return false;
}
private static Date getPrunedDate() {
Cormorant cormorant = ElectrumServer.getCormorant();
if(cormorant == null) {
return null;
}
BitcoindClient bitcoindClient = cormorant.getBitcoindClient();
if(bitcoindClient == null || !bitcoindClient.isPruned()) {
return null;
}
return bitcoindClient.getCachedPrunedDate();
}
protected void setupColumnSort(int defaultColumnIndex, TreeTableColumn.SortType defaultSortType) {
WalletTable.Sort columnSort = getSavedColumnSort();
if(columnSort == null) {
@ -248,6 +208,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
return null;
}
@SuppressWarnings("deprecation")
protected void setupColumnWidths() {
Double[] savedWidths = getSavedColumnWidths();
for(int i = 0; i < getColumns().size(); i++) {
@ -255,7 +216,8 @@ public class CoinTreeTable extends TreeTableView<Entry> {
column.setPrefWidth(savedWidths != null && getColumns().size() == savedWidths.length ? savedWidths[i] : STANDARD_WIDTH);
}
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
//TODO: Replace with TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN when JavaFX 20+ has headless support
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
getColumns().getLast().widthProperty().addListener((_, _, _) -> walletTableChanged());

View File

@ -11,7 +11,7 @@ import java.util.Date;
public class DateAxisFormatter extends StringConverter<Number> {
private static final DateFormat HOUR_FORMAT = new SimpleDateFormat("HH:mm");
private static final DateFormat DAY_FORMAT = new SimpleDateFormat("d MMM");
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yyyy");
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yy");
private final DateFormat dateFormat;
private int oddCounter;

View File

@ -14,7 +14,8 @@ import org.fxmisc.richtext.CodeArea;
import java.util.List;
import static com.sparrowwallet.drongo.policy.PolicyType.*;
import static com.sparrowwallet.drongo.policy.PolicyType.MULTI;
import static com.sparrowwallet.drongo.policy.PolicyType.SINGLE;
import static com.sparrowwallet.drongo.protocol.ScriptType.MULTISIG;
public class DescriptorArea extends CodeArea {
@ -32,13 +33,13 @@ public class DescriptorArea extends CodeArea {
List<Keystore> keystores = wallet.getKeystores();
int threshold = wallet.getDefaultPolicy().getNumSignaturesRequired();
if(SINGLE_HD.equals(policyType)) {
if(SINGLE.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
if(MULTI_HD.equals(policyType)) {
if(MULTI.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
append(MULTISIG.getDescriptor(), "descriptor-text");
append(Integer.toString(threshold), "descriptor-text");
@ -51,12 +52,6 @@ public class DescriptorArea extends CodeArea {
append(MULTISIG.getCloseDescriptor(), "descriptor-text");
append(scriptType.getCloseDescriptor(), "descriptor-text");
}
if(SINGLE_SP.equals(policyType)) {
append("sp(", "descriptor-text");
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
append(")", "descriptor-text");
}
}
public Wallet getWallet() {

View File

@ -23,10 +23,5 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, getEncoding() == QREncoding.BBQR ? bbqr : null);
event.consume();
});
ButtonBar buttonBar = (ButtonBar)dialogPane.lookup(".button-bar");
if(buttonBar != null) {
buttonBar.setButtonOrder("E+L+B+C+O");
}
}
}

View File

@ -12,7 +12,6 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@ -305,34 +304,15 @@ public class DevicePane extends TitledDescriptionPane {
if(importButton instanceof SplitMenuButton importMenuButton) {
if(wallet.getScriptType() == null) {
if(wallet.getPolicyType() == null) {
List<PolicyAndScriptType> types = new ArrayList<>();
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
types.add(new PolicyAndScriptType(policyType, scriptType));
}
}
for(PolicyAndScriptType type : types) {
MenuItem item = new MenuItem(type.getDescription());
final List<ChildNumber> derivation = type.scriptType().getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
wallet.setPolicyType(type.policyType());
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
} else {
List<ScriptType> scriptTypes = ScriptType.getScriptTypesForPolicyType(wallet.getPolicyType());
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH, ScriptType.P2TR};
for(ScriptType scriptType : scriptTypes) {
MenuItem item = new MenuItem(scriptType.getDescription());
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
item.setOnAction(event -> {
importMenuButton.setDisable(true);
importKeystore(derivation);
});
importMenuButton.getItems().add(item);
}
importMenuButton.getItems().add(new SeparatorMenuItem());
MenuItem discoverItem = new MenuItem("Discover Wallet...");
@ -731,7 +711,7 @@ public class DevicePane extends TitledDescriptionPane {
return;
}
Service<Keystore> importService = cardApi.getImportService(wallet.getPolicyType(), derivation, messageProperty);
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
handleCardOperation(importService, importButton, "Import", true, event -> {
importKeystore(derivation, importService.getValue());
});
@ -750,21 +730,13 @@ public class DevicePane extends TitledDescriptionPane {
}
}
importKey(derivation);
importXpub(derivation);
});
enumerateService.setOnFailed(workerStateEvent -> {
setError("Error", enumerateService.getException().getMessage());
importButton.setDisable(false);
});
enumerateService.start();
} else {
importKey(derivation);
}
}
private void importKey(List<ChildNumber> derivation) {
if(wallet != null && wallet.getPolicyType() == PolicyType.SINGLE_SP) {
importSpscan(derivation);
} else {
importXpub(derivation);
}
@ -775,7 +747,7 @@ public class DevicePane extends TitledDescriptionPane {
Hwi.GetXpubService getXpubService = new Hwi.GetXpubService(device, passphrase.get(), derivationPath);
getXpubService.setOnSucceeded(workerStateEvent -> {
ExtendedKey xpub = getXpubService.getValue();
String xpub = getXpubService.getValue();
try {
Keystore keystore = new Keystore();
@ -783,7 +755,7 @@ public class DevicePane extends TitledDescriptionPane {
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setExtendedPublicKey(xpub);
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
importKeystore(derivation, keystore);
} catch(Exception e) {
@ -799,44 +771,14 @@ public class DevicePane extends TitledDescriptionPane {
getXpubService.start();
}
private void importSpscan(List<ChildNumber> derivation) {
String derivationPath = KeyDerivation.writePath(derivation);
Hwi.GetSpscanService getSpscanService = new Hwi.GetSpscanService(device, passphrase.get(), derivationPath);
getSpscanService.setOnSucceeded(workerStateEvent -> {
SilentPaymentScanAddress spscan = getSpscanService.getValue();
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setSilentPaymentScanAddress(spscan);
importKeystore(derivation, keystore);
} catch(Exception e) {
setError("Could not retrieve spscan", e.getMessage());
}
});
getSpscanService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve spscan", getSpscanService.getException().getMessage());
importButton.setDisable(false);
});
setDescription("Importing...");
showHideLink.setVisible(false);
getSpscanService.start();
}
private void importKeystore(List<ChildNumber> derivation, Keystore keystore) {
if(wallet.getScriptType() == null) {
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().getFirst().equals(derivation.getFirst())).findFirst().orElse(ScriptType.P2PKH);
PolicyType policyType = wallet.getPolicyType() != null ? wallet.getPolicyType() : PolicyType.SINGLE_HD;
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
wallet.setName(device.getModel().toDisplayString());
wallet.setPolicyType(policyType);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
@ -984,7 +926,7 @@ public class DevicePane extends TitledDescriptionPane {
List<StandardAccount> discoveryAccounts = new ArrayList<>(Arrays.asList(StandardAccount.values()).subList(0, optRange.get() + 1));
Map<Hwi.WalletType, String> derivationPaths = new LinkedHashMap<>();
List<ScriptType> scriptTypes = new ArrayList<>(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD));
List<ScriptType> scriptTypes = new ArrayList<>(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE));
if(device.getModel() == WalletModel.BITBOX_02) {
scriptTypes.remove(ScriptType.P2PKH);
}
@ -996,21 +938,21 @@ public class DevicePane extends TitledDescriptionPane {
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), derivationPaths);
getXpubsService.setOnSucceeded(_ -> {
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
Map<Hwi.WalletType, String> accountXpubs = getXpubsService.getValue();
for(Map.Entry<Hwi.WalletType, ExtendedKey> entry : accountXpubs.entrySet()) {
for(Map.Entry<Hwi.WalletType, String> entry : accountXpubs.entrySet()) {
try {
Wallet wallet = new Wallet(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE_HD);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(entry.getKey().scriptType());
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPaths.get(entry.getKey())));
keystore.setExtendedPublicKey(entry.getValue());
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, entry.getKey().scriptType(), wallet.getKeystores(), 1));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, entry.getKey().scriptType(), wallet.getKeystores(), 1));
if(entry.getKey().standardAccount().equals(StandardAccount.ACCOUNT_0)) {
wallets.add(wallet);
} else {
@ -1040,7 +982,7 @@ public class DevicePane extends TitledDescriptionPane {
AppServices.showErrorDialog("No existing wallet found",
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
"You can however import the " + device.getModel().toDisplayString() + " and scan the blockchain by supplying a start date." :
"Could not find an HD wallet with existing transactions using the " + device.getModel().toDisplayString() + ".");
"Could not find a wallet with existing transactions using the " + device.getModel().toDisplayString() + ".");
setDefaultStatus();
importButton.setDisable(false);
}
@ -1090,16 +1032,16 @@ public class DevicePane extends TitledDescriptionPane {
Map<StandardAccount, Keystore> importedKeystores = new LinkedHashMap<>();
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), accountDerivationPaths);
getXpubsService.setOnSucceeded(workerStateEvent -> {
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
Map<Hwi.WalletType, String> accountXpubs = getXpubsService.getValue();
for(Map.Entry<Hwi.WalletType, ExtendedKey> entry : accountXpubs.entrySet()) {
for(Map.Entry<Hwi.WalletType, String> entry : accountXpubs.entrySet()) {
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, accountDerivationPaths.get(entry.getKey())));
keystore.setExtendedPublicKey(entry.getValue());
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
importedKeystores.put(entry.getKey().standardAccount(), keystore);
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
@ -1237,7 +1179,7 @@ public class DevicePane extends TitledDescriptionPane {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
importKey(importDerivation);
importXpub(importDerivation);
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
@ -1413,10 +1355,4 @@ public class DevicePane extends TitledDescriptionPane {
public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, GET_PRIVATE_KEY, GET_ADDRESS;
}
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
public String getDescription() {
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
}
}
}

View File

@ -24,7 +24,6 @@ public class DialogImage extends StackPane {
public ObjectProperty<DialogImage.Type> typeProperty = new SimpleObjectProperty<>();
public DialogImage() {
getStyleClass().add("dialog-image");
setPrefSize(WIDTH, HEIGHT);
this.typeProperty.addListener((observable, oldValue, type) -> {
refresh(type);

View File

@ -62,7 +62,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z");
private static final String SPARROW_RELEASE_PREFIX = "sparrow-";
private static final String[] SPARROW_RELEASE_ALT_PREFIXES = { "sparrowwallet-", "sparrowwallet_", "sparrowserver-", "sparrowserver_" };
private static final String SPARROW_RELEASE_ALT_PREFIX = "sparrow_";
private static final String SPARROW_MANIFEST_SUFFIX = "-manifest.txt";
private static final String SPARROW_SIGNATURE_SUFFIX = SPARROW_MANIFEST_SUFFIX + ".asc";
private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*");
@ -465,7 +465,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
String providedName = providedFile.getName().toLowerCase(Locale.ROOT);
if(providedName.startsWith(SPARROW_RELEASE_PREFIX) || Arrays.stream(SPARROW_RELEASE_ALT_PREFIXES).anyMatch(providedName::startsWith)) {
if(providedName.startsWith(SPARROW_RELEASE_PREFIX) || providedName.startsWith(SPARROW_RELEASE_ALT_PREFIX)) {
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
if(matcher.find()) {
String version = matcher.group();
@ -591,8 +591,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
}
}
if((name.startsWith(SPARROW_RELEASE_PREFIX) || Arrays.stream(SPARROW_RELEASE_ALT_PREFIXES).anyMatch(name::startsWith))
&& file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
if((name.startsWith(SPARROW_RELEASE_PREFIX) || name.startsWith(SPARROW_RELEASE_ALT_PREFIX)) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name);
return matcher.find();
}

View File

@ -4,7 +4,6 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
@ -136,7 +135,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
HBox actionBox = new HBox();
actionBox.getStyleClass().add("cell-actions");
if(!nodeEntry.getNode().getWallet().isBip47() && nodeEntry.getNode().getWallet().getPolicyType() != PolicyType.SINGLE_SP) {
if(!nodeEntry.getNode().getWallet().isBip47()) {
Button receiveButton = new Button("");
receiveButton.setGraphic(getReceiveGlyph());
receiveButton.setOnAction(event -> {
@ -250,7 +249,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
double vSize = tx.getVirtualSize();
if(changeTotal == 0) {
//Add change output length to vSize if change was not present on the original transaction
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().getNode(KeyPurpose.CHANGE).getOutputScript());
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getOutputScript());
vSize += changeOutput.getLength();
}
double inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? (double)tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
@ -335,10 +334,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
if(cancelTransaction) {
Payment existing = payments.get(0);
Payment payment = transactionEntry.getWallet().getPolicyType() == PolicyType.SINGLE_SP ?
new SilentPayment(transactionEntry.getWallet().getSilentPaymentScanAddress().getChangeAddress().getSilentPaymentAddress(),
existing.getLabel(), existing.getAmount(), true) :
new Payment(transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress(), existing.getLabel(), existing.getAmount(), true);
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();
@ -372,10 +369,10 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}
BlockTransactionHashIndex cpfpUtxo = ourOutputs.get(0);
Address receiveAddress = transactionEntry.getWallet().getNode(KeyPurpose.RECEIVE).getAddress();
TransactionOutput txOutput = new TransactionOutput(new Transaction(), cpfpUtxo.getValue(), receiveAddress.getOutputScript());
long dustThreshold = receiveAddress.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
double inputSize = receiveAddress.getScriptType().getInputVbytes();
Address freshAddress = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress();
TransactionOutput txOutput = new TransactionOutput(new Transaction(), cpfpUtxo.getValue(), freshAddress.getOutputScript());
long dustThreshold = freshAddress.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
double inputSize = freshAddress.getScriptType().getInputVbytes();
double vSize = inputSize + txOutput.getLength();
List<TxoFilter> txoFilters = List.of(new ExcludeTxoFilter(List.of(cpfpUtxo)), new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(transactionEntry.getWallet()));
@ -399,10 +396,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
Payment payment = transactionEntry.getWallet().getPolicyType() == PolicyType.SINGLE_SP ?
new SilentPayment(transactionEntry.getWallet().getSilentPaymentScanAddress().getChangeAddress().getSilentPaymentAddress(),
label, inputTotal, true) :
new Payment(transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress(), label, inputTotal, true);
Payment payment = new Payment(freshAddress, label, inputTotal, true);
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
@ -414,8 +408,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static boolean canSignMessage(WalletNode walletNode) {
Wallet wallet = walletNode.getWallet();
PolicyType policyType = wallet.getPolicyType();
return (policyType == PolicyType.SINGLE_HD || policyType == PolicyType.SINGLE_SP) && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
return wallet.getKeystores().size() == 1 && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
}
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
@ -670,7 +663,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
public static class AddressContextMenu extends ContextMenu {
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry, boolean addUtxoItems, TreeTableView<Entry> treetable) {
if(nodeEntry == null || (!nodeEntry.getWallet().isBip47() && nodeEntry.getWallet().getPolicyType() != PolicyType.SINGLE_SP)) {
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
MenuItem receiveToAddress = new MenuItem("Receive To");
receiveToAddress.setGraphic(getReceiveGlyph());
receiveToAddress.setOnAction(event -> {

View File

@ -25,7 +25,7 @@ public class FileKeystoreImportPane extends FileImportPane {
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
Keystore keystore = getScannedKeystore(wallet.getScriptType());
if(keystore == null) {
keystore = importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType(), inputStream, password);
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
}
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {

View File

@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.registry.RegistryItem;
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
import com.sparrowwallet.hummingbird.registry.RegistryType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
@ -32,7 +32,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.Optional;
import static com.sparrowwallet.sparrow.wallet.SettingsController.getUROutputDescriptor;
import static com.sparrowwallet.sparrow.wallet.SettingsController.getCryptoOutput;
public class FileWalletExportPane extends TitledDescriptionPane {
private final Wallet wallet;
@ -176,9 +176,9 @@ public class FileWalletExportPane extends TitledDescriptionPane {
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
QREncoding encoding = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()) ? QREncoding.BBQR : QREncoding.UR;
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
RegistryItem registryItem = getUROutputDescriptor(exportWallet);
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), registryItem.toUR(), bbqr, encoding);
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, encoding);
} else if(exporter.getClass().equals(ColdcardMultisig.class)) {
UR ur = UR.fromBytes(outputStream.toByteArray());
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());

View File

@ -30,8 +30,8 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class FileWalletKeystoreImportPane extends FileImportPane {
private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class);
@ -50,27 +50,19 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
this.fileName = fileName;
this.password = password;
List<PolicyAndScriptType> types = new ArrayList<>();
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
types.add(new PolicyAndScriptType(policyType, scriptType));
}
}
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
if(wallets != null && !wallets.isEmpty()) {
wallets.stream().filter(w -> w.getPolicyType() == null).forEach(w -> w.setPolicyType(PolicyType.SINGLE_HD));
List<PolicyAndScriptType> walletTypes = wallets.stream().map(w -> new PolicyAndScriptType(w.getPolicyType(), w.getScriptType())).toList();
types.retainAll(walletTypes);
if(types.isEmpty()) {
throw new ImportException("No singlesig script types present in QR code");
}
if(types.size() == 1) {
Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == types.getFirst().policyType() && w.getScriptType() == types.getFirst().scriptType()).findFirst().orElseThrow(ImportException::new);
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), null));
if(wallets.size() == 1 && scriptTypes.contains(wallets.get(0).getScriptType())) {
Wallet wallet = wallets.get(0);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
wallet.setName(importer.getName());
EventManager.get().post(new WalletImportEvent(wallet));
return;
EventManager.get().post(new WalletImportEvent(wallets.get(0)));
} else {
scriptTypes.retainAll(wallets.stream().map(Wallet::getScriptType).collect(Collectors.toList()));
if(scriptTypes.isEmpty()) {
throw new ImportException("No singlesig script types present in QR code");
}
}
} else {
try {
@ -80,61 +72,58 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
}
}
setContent(getScriptTypeEntry(types));
setContent(getScriptTypeEntry(scriptTypes));
setExpanded(true);
importButton.setDisable(true);
}
private void importWallet(PolicyAndScriptType type) throws ImportException {
PolicyType policyType = type.policyType();
ScriptType scriptType = type.scriptType();
private void importWallet(ScriptType scriptType) throws ImportException {
if(wallets != null && !wallets.isEmpty()) {
Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == policyType && w.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
Wallet wallet = wallets.stream().filter(wallet1 -> wallet1.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
wallet.setName(importer.getName());
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
Keystore keystore = importer.getKeystore(policyType, scriptType, bais, password);
Keystore keystore = importer.getKeystore(scriptType, bais, password);
Wallet wallet = new Wallet();
wallet.setName(Files.getNameWithoutExtension(fileName));
wallet.setPolicyType(policyType);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
}
}
private Node getScriptTypeEntry(List<PolicyAndScriptType> types) {
Label label = new Label("Type:");
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
Label label = new Label("Script Type:");
HBox fieldBox = new HBox(5);
fieldBox.setAlignment(Pos.CENTER_RIGHT);
ComboBox<PolicyAndScriptType> comboBox = new ComboBox<>(FXCollections.observableArrayList(types));
PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH);
if(types.contains(defaultType)) {
comboBox.setValue(defaultType);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
if(scriptTypes.contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
}
comboBox.setConverter(new StringConverter<>() {
scriptTypeComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(PolicyAndScriptType type) {
return type == null ? "" : type.getDescription();
public String toString(ScriptType scriptType) {
return scriptType == null ? "" : scriptType.getDescription();
}
@Override
public PolicyAndScriptType fromString(String string) {
public ScriptType fromString(String string) {
return null;
}
});
comboBox.setMaxWidth(220);
scriptTypeComboBox.setMaxWidth(170);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is newer and supports both HD and SP (silent payments) wallets.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(comboBox, helpLabel);
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
@ -144,7 +133,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
showHideLink.setVisible(true);
setExpanded(false);
try {
importWallet(comboBox.getValue());
importWallet(scriptTypeComboBox.getValue());
} catch(ImportException e) {
log.error("Error importing file", e);
String errorMessage = e.getMessage();
@ -165,14 +154,8 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
Platform.runLater(comboBox::requestFocus);
Platform.runLater(scriptTypeComboBox::requestFocus);
return contentBox;
}
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
public String getDescription() {
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
}
}
}

View File

@ -10,12 +10,7 @@ import com.sparrowwallet.drongo.crypto.Bip322;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
@ -25,6 +20,8 @@ import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
@ -160,17 +157,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
signature.setStyle("-fx-pref-height: 80px");
signature.setWrapText(true);
signature.setOnMouseClicked(event -> signature.selectAll());
ContextMenu signatureMenu = new ContextMenu();
MenuItem copyItem = new MenuItem("Copy");
copyItem.setOnAction(e -> signature.copy());
MenuItem pasteItem = new MenuItem("Paste");
pasteItem.setOnAction(e -> signature.paste());
MenuItem clearItem = new MenuItem("Clear");
clearItem.setOnAction(e -> signature.clear());
signatureMenu.getItems().addAll(copyItem, pasteItem, clearItem);
signature.setContextMenu(signatureMenu);
signatureField.getInputs().add(signature);
Field formatField = new Field();
@ -256,7 +242,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
if(wallet != null) {
setWalletNodeFromAddress(wallet, address);
if(walletNode != null) {
setFormatFromScriptType(walletNode.getWallet().getScriptType());
setFormatFromScriptType(getSigningScriptType(walletNode));
}
}
} catch(InvalidAddressException e) {
@ -264,13 +250,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
}
});
formatGroup.selectedToggleProperty().addListener((_, _, newVal) -> {
if(wallet != null) {
boolean canSignSelectedFormat = canSignAllFormats(wallet) || newVal == formatElectrum;
signButton.setDisable(!isValidAddress() || !canSign || !canSignSelectedFormat);
}
});
}
EventManager.get().register(this);
@ -298,7 +277,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
if(wallet != null && walletNode != null) {
setFormatFromScriptType(walletNode.getWallet().getScriptType());
setFormatFromScriptType(getSigningScriptType(walletNode));
} else {
formatGroup.selectToggle(formatElectrum);
}
@ -306,8 +285,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
private void checkWalletSigning(Wallet wallet) {
if(wallet.getKeystores().size() != 1 || (wallet.getPolicyType() != PolicyType.SINGLE_HD && wallet.getPolicyType() != PolicyType.SINGLE_SP)) {
throw new IllegalArgumentException("Cannot sign messages using this wallet type");
if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
}
}
@ -317,7 +296,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|| wallet.getKeystores().getFirst().getWalletModel().isCard();
}
private boolean canSignAllFormats(Wallet wallet) {
private boolean canSignBip322(Wallet wallet) {
return wallet.getKeystores().getFirst().hasPrivateKey();
}
@ -332,7 +311,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private boolean isValidAddress() {
try {
Address address = getAddress();
return address.getScriptType().isAllowed(PolicyType.SINGLE_HD) || address.getScriptType() == ScriptType.P2SH;
return address.getScriptType().isAllowed(PolicyType.SINGLE) || address.getScriptType() == ScriptType.P2SH;
} catch (InvalidAddressException e) {
return false;
}
@ -342,6 +321,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
walletNode = wallet.getWalletAddresses().get(address);
}
private ScriptType getSigningScriptType(WalletNode walletNode) {
ScriptType scriptType = walletNode.getWallet().getScriptType();
return canSign(walletNode.getWallet()) && !canSignBip322(walletNode.getWallet()) ? ScriptType.P2PKH : scriptType;
}
private void setFormatFromScriptType(ScriptType scriptType) {
formatElectrum.setDisable(scriptType == ScriptType.P2TR);
formatTrezor.setDisable(scriptType == ScriptType.P2TR || scriptType == ScriptType.P2PKH);
@ -388,24 +372,18 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private void signUnencryptedKeystore(Wallet decryptedWallet) {
try {
Keystore keystore = decryptedWallet.getKeystores().getFirst();
ECKey privKey = keystore.getKey(walletNode);
String signatureText;
if(decryptedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
ECKey spendPrivKey = keystore.getSpendPrivateKey(Collections.emptyMap());
signatureText = Bip322.signMessageBip322Sp(walletNode.getAddress(), message.getText().trim(), spendPrivKey, walletNode.getSilentPaymentTweak());
spendPrivKey.clear();
if(isBip322()) {
ScriptType scriptType = decryptedWallet.getScriptType();
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
} else {
ECKey privKey = keystore.getKey(walletNode);
if(isBip322()) {
ScriptType scriptType = decryptedWallet.getScriptType();
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
} else {
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
}
privKey.clear();
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
}
signature.clear();
signature.appendText(signatureText);
privKey.clear();
} catch(Exception e) {
log.error("Could not sign message", e);
AppServices.showErrorDialog("Could not sign message", e.getMessage());
@ -481,11 +459,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
if(scriptType == ScriptType.P2SH) {
scriptType = ScriptType.P2SH_P2WPKH;
}
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).contains(scriptType)) {
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).contains(scriptType)) {
throw new IllegalArgumentException("Only single signature P2PKH, P2SH-P2WPKH or P2WPKH addresses can verify messages.");
}
Address signedMessageAddress = scriptType.getAddress(PolicyType.SINGLE_HD, signedMessageKey);
Address signedMessageAddress = scriptType.getAddress(signedMessageKey);
return providedAddress.equals(signedMessageAddress);
}
@ -495,11 +473,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
return;
}
if(isBip322()) {
showBip322Qr();
return;
}
//Note we can expect a single keystore due to the check in the constructor
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
String derivationPath = KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), false);
@ -513,88 +486,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
}
private void showBip322Qr() {
Wallet signingWallet = walletNode.getWallet();
PSBT psbt = buildBip322Psbt(signingWallet);
byte[] psbtBytes = psbt.getForExport().serialize();
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, QREncoding.UR);
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
scanQr();
}
}
private PSBT buildBip322Psbt(Wallet signingWallet) {
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
Keystore keystore = signingWallet.getKeystores().getFirst();
ECKey spendPubKey = keystore.getSilentPaymentScanAddress().getSpendKey();
KeyDerivation spendDerivation = new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), KeyDerivation.writePath(KeyDerivation.getBip352SpendDerivation(keystore.getKeyDerivation().getDerivation())));
return Bip322.getBip322PsbtSp(walletNode.getAddress(), message.getText().trim(), walletNode.getSilentPaymentTweak(), Map.of(spendPubKey, spendDerivation));
}
PSBT psbt = Bip322.getBip322Psbt(signingWallet.getScriptType(), walletNode.getAddress(), message.getText().trim());
addBip322DerivationInfo(psbt, signingWallet);
return psbt;
}
private String extractBip322Signature(PSBT signedPsbt) {
String psbtMessage = signedPsbt.getGenericSignedMessage();
if(psbtMessage != null && !psbtMessage.equals(message.getText().trim())) {
Optional<ButtonType> response = AppServices.showWarningDialog("Message mismatch",
"The message in the signed PSBT does not match the message in this dialog.\n\nPSBT message: " + psbtMessage +
"\n\nContinue extracting the signature?", ButtonType.NO, ButtonType.YES);
if(response.isEmpty() || response.get() != ButtonType.YES) {
return null;
}
}
Wallet signingWallet = walletNode.getWallet();
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
return Bip322.getBip322SignatureFromPsbtSp(signedPsbt);
}
ECKey pubKey = signingWallet.getKeystores().getFirst().getPubKey(walletNode);
return Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), signedPsbt, pubKey);
}
private void addBip322DerivationInfo(PSBT psbt, Wallet signingWallet) {
ScriptType scriptType = signingWallet.getScriptType();
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
Keystore keystore = signingWallet.getKeystores().get(0);
ECKey pubKey = keystore.getPubKey(walletNode);
KeyDerivation fullDerivation = keystore.getKeyDerivation().extend(walletNode.getDerivation());
if(scriptType == ScriptType.P2TR) {
psbtInput.setTapInternalKey(pubKey);
psbtInput.getTapDerivedPublicKeys().put(ECKey.fromPublicOnly(pubKey.getPubKeyXCoord()), Map.of(fullDerivation, Collections.emptyList()));
} else {
psbtInput.getDerivedPublicKeys().put(scriptType.getOutputKey(signingWallet.getPolicyType(), pubKey), fullDerivation);
}
}
private void scanQr() {
QRScanDialog qrScanDialog = new QRScanDialog();
qrScanDialog.initOwner(getDialogPane().getScene().getWindow());
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.psbt != null) {
try {
String sig = extractBip322Signature(result.psbt);
if(sig != null) {
signature.clear();
signature.appendText(sig);
}
} catch(Exception e) {
log.error("Error extracting BIP-322 signature from PSBT", e);
AppServices.showErrorDialog("Error extracting signature", e.getMessage());
}
} else if(result.payload != null) {
if(result.payload != null) {
signature.clear();
signature.appendText(result.payload);
} else if(result.exception != null) {
@ -612,11 +510,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
return;
}
if(isBip322()) {
exportBip322File();
return;
}
StringJoiner joiner = new StringJoiner("\n");
joiner.add(message.getText().trim().replaceAll("\r*\n*", ""));
//Note we can expect a single keystore due to the check in the constructor
@ -645,65 +538,20 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
}
private void exportBip322File() {
Wallet signingWallet = walletNode.getWallet();
PSBT psbt = buildBip322Psbt(signingWallet);
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save PSBT File");
fileChooser.setInitialFileName("bip322-signmessage.psbt");
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
try(OutputStream os = new FileOutputStream(file)) {
os.write(psbt.getForExport().serialize());
} catch(IOException e) {
log.error("Error saving BIP-322 PSBT", e);
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
}
}
}
private void importFile() {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open Signed File");
fileChooser.setTitle("Open Signed Text File");
fileChooser.getExtensionFilters().addAll(
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
new FileChooser.ExtensionFilter("Text Files", "*.txt"),
new FileChooser.ExtensionFilter("PSBT Files", "*.psbt")
new FileChooser.ExtensionFilter("Text Files", "*.txt")
);
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showOpenDialog(window);
if(file != null) {
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt") || isBip322()) {
if(walletNode == null) {
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
return;
}
try {
byte[] psbtBytes = Files.readAllBytes(file.toPath());
PSBT signedPsbt = new PSBT(psbtBytes, false);
String sig = extractBip322Signature(signedPsbt);
if(sig != null) {
signature.clear();
signature.appendText(sig);
}
return;
} catch(Exception e) {
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt")) {
log.error("Error loading signed PSBT", e);
AppServices.showErrorDialog("Error loading signed PSBT", e.getMessage());
return;
}
//Fall through to text handling for non-.psbt files
}
}
try {
String content = Files.readString(file.toPath(), StandardCharsets.UTF_8);
Matcher matcher = signedMessagePattern.matcher(content);

View File

@ -141,7 +141,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
if(!empty && validWords) {
try {
importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
@ -256,7 +256,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
importButton.setDisable(true);
try {
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, wordEntriesProperty.get(), passphraseProperty.get());
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
if(!dryrun) {
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);

View File

@ -162,7 +162,7 @@ public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
existing.add(wordEntriesProperty.get());
}
importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType().getDefaultDerivation(), existing, passphraseProperty.get());
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), existing, passphraseProperty.get());
validSet = true;
complete = true;
} catch(MnemonicException e) {
@ -240,7 +240,7 @@ public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
importButton.setDisable(true);
try {
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, mnemonicShares, passphraseProperty.get());
Keystore keystore = importer.getKeystore(derivation, mnemonicShares, passphraseProperty.get());
if(!dryrun) {
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);

View File

@ -81,7 +81,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
if(!empty && validWords) {
try {
importer.getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
importer.getKeystore(ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
validChecksum = true;
} catch(ImportException e) {
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
@ -108,14 +108,14 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
discoverButton.setGraphic(progressIndicator);
List<Wallet> wallets = new ArrayList<>();
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
derivations.add(List.of(new ChildNumber(0, true)));
derivations.add(ScriptType.P2PKH.getDefaultDerivation(1)); //Bisq segwit misderivation
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD)) {
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE)) {
for(List<ChildNumber> derivation : derivations) {
try {
Wallet wallet = getWallet(PolicyType.SINGLE_HD, scriptType, derivation);
Wallet wallet = getWallet(scriptType, derivation);
wallets.add(wallet);
} catch(ImportException e) {
String errorMessage = e.getMessage();
@ -148,7 +148,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found",
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
"You can however import this wallet and scan the blockchain by supplying a start date. Do you want to import this wallet?" :
"Could not find an HD wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
"Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
setContent(getScriptTypeEntry());
setExpanded(true);
@ -163,49 +163,41 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
walletDiscoveryService.start();
}
private Wallet getWallet(PolicyType policyType, ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
private Wallet getWallet(ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
Wallet wallet = new Wallet("");
wallet.setPolicyType(policyType);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(scriptType);
Keystore keystore = importer.getKeystore(policyType, derivation, wordEntriesProperty.get(), passphraseProperty.get());
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), 1));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
return wallet;
}
private Node getScriptTypeEntry() {
Label label = new Label("Type:");
List<PolicyAndScriptType> types = new ArrayList<>();
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
types.add(new PolicyAndScriptType(policyType, scriptType));
}
}
Label label = new Label("Script Type:");
HBox fieldBox = new HBox(5);
fieldBox.setAlignment(Pos.CENTER_RIGHT);
ComboBox<PolicyAndScriptType> comboBox = new ComboBox<>(FXCollections.observableArrayList(types));
PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH);
if(types.contains(defaultType)) {
comboBox.setValue(defaultType);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
if(scriptTypeComboBox.getItems().contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
}
comboBox.setConverter(new StringConverter<>() {
scriptTypeComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(PolicyAndScriptType type) {
return type == null ? "" : type.getDescription();
public String toString(ScriptType scriptType) {
return scriptType == null ? "" : scriptType.getDescription();
}
@Override
public PolicyAndScriptType fromString(String string) {
public ScriptType fromString(String string) {
return null;
}
});
comboBox.setMaxWidth(220);
scriptTypeComboBox.setMaxWidth(170);
HelpLabel helpLabel = new HelpLabel();
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nTaproot Silent Payments creates a silent payment wallet.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(comboBox, helpLabel);
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
@ -216,8 +208,8 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
showHideLink.setVisible(true);
setExpanded(false);
try {
PolicyAndScriptType type = comboBox.getValue();
Wallet wallet = getWallet(type.policyType(), type.scriptType(), type.scriptType().getDefaultDerivation());
ScriptType scriptType = scriptTypeComboBox.getValue();
Wallet wallet = getWallet(scriptType, scriptType.getDefaultDerivation());
EventManager.get().post(new WalletImportEvent(wallet));
} catch(ImportException e) {
log.error("Error importing mnemonic", e);
@ -239,10 +231,4 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
return contentBox;
}
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
public String getDescription() {
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
}
}
}

View File

@ -14,8 +14,6 @@ import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTProofException;
import com.sparrowwallet.drongo.silentpayments.*;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.AppServices;
@ -68,7 +66,6 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
private final ComboBox<Wallet> toWallet;
private final FeeRangeSlider feeRange;
private final CopyableLabel feeRate;
private SilentPaymentAddress silentPaymentAddress;
public PrivateKeySweepDialog(Wallet wallet) {
final DialogPane dialogPane = getDialogPane();
@ -112,7 +109,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
Field keyScriptTypeField = new Field();
keyScriptTypeField.setText("Script Type:");
keyScriptType = new ComboBox<>();
keyScriptType.setItems(FXCollections.observableList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD)));
keyScriptType.setItems(FXCollections.observableList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
keyScriptTypeField.getInputs().add(keyScriptType);
keyScriptType.setConverter(new StringConverter<ScriptType>() {
@ -207,31 +204,18 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
});
toAddress.textProperty().addListener((observable, oldValue, newValue) -> {
try {
silentPaymentAddress = SilentPaymentAddress.from(newValue);
} catch(Exception e) {
silentPaymentAddress = null;
}
createButton.setDisable(!isValidKey() || !isValidToAddress());
});
toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> {
if(selectedWallet != null) {
if(selectedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
toAddress.setText(selectedWallet.getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
} else {
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
}
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
}
});
keyScriptType.setValue(ScriptType.P2PKH);
if(wallet != null) {
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
toAddress.setText(wallet.getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
} else {
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
}
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
}
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));
@ -288,13 +272,10 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
}
private boolean isValidToAddress() {
if(silentPaymentAddress != null) {
return true;
}
try {
getToAddress();
Address address = getToAddress();
return true;
} catch(InvalidAddressException e) {
} catch (InvalidAddressException e) {
return false;
}
}
@ -306,14 +287,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
private void setFromAddress() {
DumpedPrivateKey privateKey = getPrivateKey();
ScriptType scriptType = keyScriptType.getValue();
Address address = scriptType.getAddress(PolicyType.SINGLE_HD, privateKey.getKey());
Address address = scriptType.getAddress(privateKey.getKey());
keyAddress.setText(address.toString());
}
private void setScriptTypes(boolean isValidKey) {
boolean compressed = !isValidKey || getPrivateKey().getKey().isCompressed();
if(compressed && !keyScriptType.getItems().equals(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD))) {
keyScriptType.getItems().addAll(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD).stream().filter(s -> !keyScriptType.getItems().contains(s)).collect(Collectors.toList()));
if(compressed && !keyScriptType.getItems().equals(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE))) {
keyScriptType.getItems().addAll(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE).stream().filter(s -> !keyScriptType.getItems().contains(s)).collect(Collectors.toList()));
} else if(!compressed && !keyScriptType.getItems().equals(List.of(ScriptType.P2PKH))) {
keyScriptType.getSelectionModel().select(0);
keyScriptType.getItems().removeIf(scriptType -> scriptType != ScriptType.P2PKH);
@ -365,9 +346,8 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
try {
DumpedPrivateKey privateKey = getPrivateKey();
ScriptType scriptType = keyScriptType.getValue();
Address fromAddress = scriptType.getAddress(PolicyType.SINGLE_HD, privateKey.getKey());
Payment payment = silentPaymentAddress != null ? new SilentPayment(silentPaymentAddress, null, 0, true)
: new Payment(getToAddress(), null, 0, true);
Address fromAddress = scriptType.getAddress(privateKey.getKey());
Address destAddress = getToAddress();
Date since = null;
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
@ -383,7 +363,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress, since);
addressUtxosService.setOnSucceeded(successEvent -> {
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), payment);
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
});
addressUtxosService.setOnFailed(failedEvent -> {
Throwable rootCause = Throwables.getRootCause(failedEvent.getSource().getException());
@ -403,14 +383,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
}
}
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Payment payment) {
Address destAddress = payment instanceof SilentPayment silentPayment ? computeSilentPaymentAddress(privKey, scriptType, txOutputs, silentPayment) : payment.getAddress();
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Address destAddress) {
ECKey pubKey = ECKey.fromPublicOnly(privKey);
Transaction noFeeTransaction = new Transaction();
long total = 0;
for(TransactionOutput txOutput : txOutputs) {
scriptType.addSpendingInput(PolicyType.SINGLE_HD, noFeeTransaction, txOutput, pubKey, TransactionSignature.dummy(scriptType == P2TR ? TransactionSignature.Type.SCHNORR : TransactionSignature.Type.ECDSA));
scriptType.addSpendingInput(noFeeTransaction, txOutput, pubKey, TransactionSignature.dummy(scriptType == P2TR ? TransactionSignature.Type.SCHNORR : TransactionSignature.Type.ECDSA));
total += txOutput.getValue();
}
@ -469,7 +448,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
psbtInput.setWitnessScript(txInput.getWitness().getWitnessScript());
}
if(!psbtInput.sign(scriptType.getOutputKey(PolicyType.SINGLE_HD, privKey))) {
if(!psbtInput.sign(scriptType.getOutputKey(privKey))) {
AppServices.showErrorDialog("Failed to sign", "Failed to sign for transaction output " + utxoOutput.getHash() + ":" + utxoOutput.getIndex());
return;
}
@ -477,7 +456,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
Transaction finalizeTransaction = new Transaction();
TransactionInput finalizedTxInput = scriptType.addSpendingInput(PolicyType.SINGLE_HD, finalizeTransaction, utxoOutput, pubKey, signature);
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
psbtInput.setFinalScriptSig(finalizedTxInput.getScriptSig());
psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness());
}
@ -489,29 +468,6 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
}
}
private Address computeSilentPaymentAddress(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, SilentPayment silentPayment) {
ECKey summedPrivateKey = scriptType.getOutputKey(PolicyType.SINGLE_HD, privKey);
if(scriptType == P2TR && summedPrivateKey.hasOddYCoord()) {
summedPrivateKey = summedPrivateKey.negatePrivate();
}
Set<HashIndex> outpoints = new LinkedHashSet<>();
for(TransactionOutput txOutput : txOutputs) {
outpoints.add(new HashIndex(txOutput.getHash(), txOutput.getIndex()));
}
try {
SilentPaymentUtils.computeOutputAddresses(List.of(silentPayment), summedPrivateKey, outpoints);
if(!silentPayment.isAddressComputed()) {
throw new IllegalStateException("Failed to compute silent payment address");
}
return silentPayment.getAddress();
} catch(InvalidSilentPaymentException e) {
throw new IllegalStateException("Failed to compute silent payment address", e);
}
}
public Glyph getGlyph(FontAwesome5.Glyph glyphEnum) {
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphEnum);
glyph.setFontSize(12);

View File

@ -51,7 +51,6 @@ import org.openpnp.capture.CaptureDevice;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
@ -583,14 +582,6 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
}
}
private ECKey getKey(CryptoHDKey cryptoHDKey) {
if(cryptoHDKey.isPrivateKey()) {
return ECKey.fromPrivate(new BigInteger(1, cryptoHDKey.getKey()));
} else {
return ECKey.fromPublicOnly(cryptoHDKey.getKey());
}
}
private OutputDescriptor getOutputDescriptor(CryptoOutput cryptoOutput) {
ScriptType scriptType = getScriptType(cryptoOutput.getScriptExpressions());
@ -688,16 +679,11 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
for(int i = 0; i < keys.size(); i++) {
RegistryItem key = keys.get(i);
if(key instanceof URHDKey urhdKey) {
ExtendedKey extendedKey = getExtendedKey(urhdKey);
KeyDerivation keyDerivation = getKeyDerivation(urhdKey.getOrigin());
if(urhdKey.getChainCode() == null) {
ECKey ecKey = getKey(urhdKey);
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(ecKey, keyDerivation, true, true));
} else {
ExtendedKey extendedKey = getExtendedKey(urhdKey);
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
if(urhdKey.getName() != null) {
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
}
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
if(urhdKey.getName() != null) {
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
}
} else {
throw new IllegalArgumentException("Only extended HD keys are supported in output descriptors");

View File

@ -9,13 +9,13 @@ import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.dns.DnsPaymentResolver;
import com.sparrowwallet.drongo.dns.DnsPaymentValidationException;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.io.Config;
@ -28,8 +28,6 @@ import javafx.event.ActionEvent;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.Clipboard;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.util.StringConverter;
@ -37,26 +35,20 @@ import org.controlsfx.control.spreadsheet.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class SendToManyDialog extends Dialog<List<Payment>> {
private final BitcoinUnit bitcoinUnit;
private final UnitFormat unitFormat;
private final UnitFormatDoubleCellType amountCellType;
private final SpreadsheetView spreadsheetView;
public static final SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType();
public SendToManyDialog(BitcoinUnit bitcoinUnit, UnitFormat unitFormat, List<Payment> payments) {
public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
this.bitcoinUnit = bitcoinUnit;
this.unitFormat = unitFormat == null ? UnitFormat.DOT : unitFormat;
this.amountCellType = new UnitFormatDoubleCellType(this.unitFormat);
final DialogPane dialogPane = new SendToManyDialogPane();
setDialogPane(dialogPane);
@ -127,9 +119,11 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
addressCell.getStyleClass().add("fixed-width");
list.add(addressCell);
long rawAmount = sendToPayment.payment().getAmount();
Double amount = rawAmount < 0 ? null : bitcoinUnit.getValue(rawAmount);
SpreadsheetCell amountCell = amountCellType.createCell(row, 1, 1, 1, amount);
double amount = (double)sendToPayment.payment().getAmount();
if(bitcoinUnit == BitcoinUnit.BTC) {
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
}
SpreadsheetCell amountCell = SpreadsheetCellType.DOUBLE.createCell(row, 1, 1, 1, amount < 0 ? null : amount);
amountCell.setFormat(bitcoinUnit == BitcoinUnit.BTC ? "0.00000000" : "###,###");
amountCell.getStyleClass().add("number-value");
if(OsType.getCurrent() == OsType.MACOS) {
@ -183,7 +177,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
for(int row = 0; row < spreadsheetView.getGrid().getRowCount(); row++) {
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
SendToAddress sendToAddress = (SendToAddress)rowCells.getFirst().getItem();
if(sendToAddress != null && sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
if(sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
return true;
}
}
@ -222,15 +216,12 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}
try {
String rawAmount = csvReader.get(1).trim();
String groupingStripped = rawAmount.replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "");
long amount;
if(bitcoinUnit == BitcoinUnit.BTC) {
String normalised = groupingStripped.replaceAll(Pattern.quote(unitFormat.getDecimalSeparator()), ".");
double doubleAmount = Double.parseDouble(normalised);
amount = bitcoinUnit.getSatsValue(doubleAmount);
double doubleAmount = Double.parseDouble(csvReader.get(1).replace(",", ""));
amount = (long)(doubleAmount * Transaction.SATOSHIS_PER_BITCOIN);
} else {
amount = Long.parseLong(groupingStripped);
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
}
String label = csvReader.get(2);
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
@ -368,160 +359,6 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}
};
private static class UnitFormatDoubleCellType extends SpreadsheetCellType<Double> {
private final UnitFormat unitFormat;
UnitFormatDoubleCellType(UnitFormat unitFormat) {
super(new UnitFormatDoubleConverter(unitFormat));
this.unitFormat = unitFormat;
}
@Override
public String toString() {
return "double";
}
public SpreadsheetCell createCell(int row, int column, int rowSpan, int columnSpan, Double value) {
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
cell.setItem(value);
return cell;
}
@Override
public SpreadsheetCellEditor createEditor(SpreadsheetView view) {
return new UnitFormatDoubleEditor(view, unitFormat);
}
@Override
public boolean match(Object value, Object... options) {
if(value == null || value instanceof Number) {
return true;
}
try {
String s = value.toString();
return s == null || s.isEmpty() || converter.fromString(s) != null;
} catch(Exception e) {
return false;
}
}
@Override
public Double convertValue(Object value) {
if(value instanceof Double d) {
return d;
}
if(value instanceof Number n) {
return n.doubleValue();
}
return converter.fromString(value == null ? null : value.toString());
}
@Override
public String toString(Double item) {
return converter.toString(item);
}
@Override
public String toString(Double item, String format) {
return ((StringConverterWithFormat<Double>)converter).toStringFormat(item, format);
}
}
private static class UnitFormatDoubleConverter extends StringConverterWithFormat<Double> {
private final UnitFormat unitFormat;
UnitFormatDoubleConverter(UnitFormat unitFormat) {
this.unitFormat = unitFormat;
}
@Override
public Double fromString(String str) {
if(str == null || str.isEmpty()) {
return null;
}
String normalised = str.trim()
.replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "")
.replaceAll(Pattern.quote(unitFormat.getDecimalSeparator()), ".");
try {
return Double.valueOf(normalised);
} catch(NumberFormatException e) {
return null;
}
}
@Override
public String toString(Double item) {
return toStringFormat(item, "");
}
@Override
public String toStringFormat(Double item, String format) {
if(item == null || item.isNaN()) {
return "";
}
if(format == null || format.isEmpty()) {
return Double.toString(item);
}
return new DecimalFormat(format, unitFormat.getDecimalFormatSymbols()).format(item);
}
}
private static class UnitFormatDoubleEditor extends SpreadsheetCellEditor {
private final UnitFormat unitFormat;
private final TextField textField;
UnitFormatDoubleEditor(SpreadsheetView view, UnitFormat unitFormat) {
super(view);
this.unitFormat = unitFormat;
this.textField = new TextField();
this.textField.setTextFormatter(new CoinTextFormatter(unitFormat));
}
@Override
public void startEdit(Object item, String format, Object... options) {
if(item instanceof Double d && !d.isNaN()) {
String text = (format == null || format.isEmpty())
? Double.toString(d)
: new DecimalFormat(format, unitFormat.getDecimalFormatSymbols()).format(d);
textField.setText(text);
} else {
textField.setText("");
}
textField.getStyleClass().removeAll("error");
textField.setOnKeyPressed(this::onKeyPressed);
textField.requestFocus();
textField.selectAll();
}
@Override
public void end() {
textField.setOnKeyPressed(null);
textField.setOnKeyReleased(null);
textField.getStyleClass().removeAll("error");
}
@Override
public TextField getEditor() {
return textField;
}
@Override
public String getControlValue() {
String raw = textField.getText();
return raw == null ? "" : raw.trim();
}
private void onKeyPressed(KeyEvent event) {
if(event.getCode() == KeyCode.ENTER) {
endEdit(true);
event.consume();
} else if(event.getCode() == KeyCode.ESCAPE) {
endEdit(false);
event.consume();
}
}
}
public static class SendToAddress {
private final String hrn;
private final Address address;
@ -650,7 +487,11 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}
if(sendToAddress != null && value != null) {
payments.add(sendToAddress.toPayment(label, bitcoinUnit.getSatsValue(value), false));
if(bitcoinUnit == BitcoinUnit.BTC) {
value = value * Transaction.SATOSHIS_PER_BITCOIN;
}
payments.add(sendToAddress.toPayment(label, value.longValue(), false));
}
}

View File

@ -7,7 +7,6 @@ import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.dns.DnsPayment;
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
@ -229,7 +228,7 @@ public class TransactionDiagram extends GridPane {
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(), diagram.getDisplayedOutputs().size() + 1);
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);
@ -257,12 +256,12 @@ public class TransactionDiagram extends GridPane {
Pane txPane = getTransactionPane();
GridPane.setConstraints(txPane, 3, 0);
List<WalletTransaction.Output> displayedOutputs = getDisplayedOutputs();
List<Payment> displayedPayments = getDisplayedPayments();
Pane outputsLinesPane = getOutputsLines(displayedOutputs);
Pane outputsLinesPane = getOutputsLines(displayedPayments);
GridPane.setConstraints(outputsLinesPane, 4, 0);
Pane outputsPane = getOutputsLabels(displayedOutputs);
Pane outputsPane = getOutputsLabels(displayedPayments);
GridPane.setConstraints(outputsPane, 5, 0);
getChildren().clear();
@ -653,48 +652,33 @@ public class TransactionDiagram extends GridPane {
return value * (1.0 - scaleFactor) + additional;
}
private List<WalletTransaction.Output> getDisplayedOutputs() {
List<WalletTransaction.Output> outputs = walletTx.getOutputs().stream().filter(o -> !(o instanceof WalletTransaction.NonAddressOutput)).toList();
private List<Payment> getDisplayedPayments() {
List<Payment> payments = walletTx.getPayments();
int maxPayments = getMaxPayments();
long paginableCount = outputs.stream().filter(this::isPaymentAndNotChange).count();
if(paginableCount <= maxPayments) {
return outputs;
}
List<WalletTransaction.Output> displayedOutputs = new ArrayList<>();
List<Payment> additional = new ArrayList<>();
int kept = 0;
int additionalIdx = 0;
for(WalletTransaction.Output output : outputs) {
if(isPaymentAndNotChange(output)) {
if(kept < maxPayments - 1) {
displayedOutputs.add(output);
kept++;
additionalIdx = displayedOutputs.size();
if(payments.size() > maxPayments) {
List<Payment> displayedPayments = new ArrayList<>();
List<Payment> additional = new ArrayList<>();
for(Payment payment : payments) {
if(displayedPayments.size() < maxPayments - 1) {
displayedPayments.add(payment);
} else {
additional.add(output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment());
additional.add(payment);
}
} else {
displayedOutputs.add(output);
}
displayedPayments.add(new AdditionalPayment(additional));
return displayedPayments;
} else {
return payments;
}
Payment additionalPayment = new AdditionalPayment(additional);
TransactionOutput additionalOutput = new TransactionOutput(null, additionalPayment.getAmount(), new byte[0]);
displayedOutputs.add(additionalIdx, new WalletTransaction.PaymentOutput(additionalOutput, additionalPayment));
return displayedOutputs;
}
boolean isPaymentAndNotChange(WalletTransaction.Output output) {
return (output instanceof WalletTransaction.PaymentOutput && !(output instanceof WalletTransaction.SilentPaymentChangeOutput)) || output instanceof WalletTransaction.ConsolidationOutput;
}
private List<Payment> getUserPayments() {
return walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT || payment.getType() == Payment.Type.ANCHOR).toList();
}
private Pane getOutputsLines(List<WalletTransaction.Output> displayedOutputs) {
private Pane getOutputsLines(List<Payment> displayedPayments) {
VBox pane = new VBox();
Group group = new Group();
VBox.setVgrow(group, Priority.ALWAYS);
@ -709,9 +693,10 @@ public class TransactionDiagram extends GridPane {
double width = 140.0;
long sum = walletTx.getTotal();
List<Long> values = displayedOutputs.stream().map(o -> o.getTransactionOutput().getValue()).collect(Collectors.toList());
List<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
.map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
values.add(walletTx.getFee());
int numOutputs = displayedOutputs.size() + 1;
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
for(int i = 1; i <= numOutputs; i++) {
CubicCurve curve = new CubicCurve();
curve.getStyleClass().add("output-line");
@ -743,21 +728,121 @@ public class TransactionDiagram extends GridPane {
return pane;
}
private Pane getOutputsLabels(List<WalletTransaction.Output> displayedOutputs) {
private Pane getOutputsLabels(List<Payment> displayedPayments) {
VBox outputsBox = new VBox();
outputsBox.setPadding(new Insets(0, 20, 0, 10));
outputsBox.setAlignment(Pos.BASELINE_LEFT);
outputsBox.getChildren().add(createSpacer());
List<OutputNode> outputNodes = new ArrayList<>();
for(WalletTransaction.Output output : displayedOutputs) {
if(output instanceof WalletTransaction.SilentPaymentChangeOutput spChangeOutput) {
outputNodes.add(buildSpChangeNode(spChangeOutput));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
outputNodes.add(buildHdChangeNode(changeOutput));
} else if(output instanceof WalletTransaction.PaymentOutput || output instanceof WalletTransaction.ConsolidationOutput) {
outputNodes.add(buildPaymentNode(output));
for(Payment payment : displayedPayments) {
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null;
boolean addressLabel = payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX;
Label recipientLabel = new Label(addressLabel ? payment.toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
if(addressLabel) {
recipientLabel.setSkin(new AddressLabelSkin(recipientLabel));
}
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
Wallet toBip47Wallet = getBip47SendWallet(payment);
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
+ getCoinValue(payment.getAmount()) + " to "
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()) + "\n" + payment.getDisplayAddress())
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientTooltip.setShowDuration(Duration.INDEFINITE);
recipientTooltip.setWrapText(true);
recipientTooltip.setSkin(new AddressTooltipSkin(recipientTooltip));
Window activeWindow = AppServices.getActiveWindow();
if(activeWindow != null) {
recipientTooltip.setMaxWidth(activeWindow.getWidth());
}
recipientLabel.setTooltip(recipientTooltip);
HBox paymentBox = new HBox();
paymentBox.setAlignment(Pos.CENTER_LEFT);
paymentBox.getChildren().add(recipientLabel);
if(isExpanded()) {
recipientLabel.setMinWidth(120);
Region region = new Region();
region.setMinWidth(20);
HBox.setHgrow(region, Priority.ALWAYS);
CoinLabel amountLabel = new CoinLabel();
amountLabel.setValue(payment.getAmount());
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
paymentBox.getChildren().addAll(region, amountLabel);
}
if(payment instanceof SilentPayment silentPayment) {
outputNodes.add(new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress()));
} else {
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null));
}
}
Set<Integer> seenIndexes = new HashSet<>();
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
WalletNode changeNode = changeEntry.getKey();
WalletNode defaultChangeNode = walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE);
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
HBox actionBox = new HBox();
actionBox.setAlignment(Pos.CENTER_LEFT);
Address changeAddress = walletTx.getChangeAddress(changeNode);
String changeDesc = changeAddress.toString().substring(0, 8) + "...";
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
changeLabel.getStyleClass().addAll("output-label", "change-label");
changeLabel.setSkin(new AddressLabelSkin(changeLabel));
Tooltip changeTooltip = new Tooltip("Change of " + getCoinValue(changeEntry.getValue()) + " to " + changeNode + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
changeTooltip.getStyleClass().add("change-label");
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
changeTooltip.setShowDuration(Duration.INDEFINITE);
changeTooltip.setSkin(new AddressTooltipSkin(changeTooltip));
changeLabel.setTooltip(changeTooltip);
actionBox.getChildren().add(changeLabel);
if(!isFinal()) {
Button nextChangeAddressButton = new Button("");
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
nextChangeAddressButton.setOnAction(event -> {
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
});
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
replaceChangeLabel.getStyleClass().add("replace-change-label");
replaceChangeLabel.setVisible(false);
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
actionBox.getChildren().add(replaceChangeLabel);
}
if(isExpanded()) {
changeLabel.setMinWidth(120);
Region region = new Region();
region.setMinWidth(20);
HBox.setHgrow(region, Priority.ALWAYS);
CoinLabel amountLabel = new CoinLabel();
amountLabel.setValue(changeEntry.getValue());
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
actionBox.getChildren().addAll(region, amountLabel);
}
int changeIndex = outputNodes.size();
if(isFinal()) {
changeIndex = getOutputIndex(changeAddress, changeEntry.getValue(), seenIndexes);
seenIndexes.add(changeIndex);
if(changeIndex > outputNodes.size()) {
changeIndex = outputNodes.size();
}
}
outputNodes.add(changeIndex, new OutputNode(actionBox, changeAddress, changeEntry.getValue()));
}
for(OutputNode outputNode : outputNodes) {
@ -804,143 +889,6 @@ public class TransactionDiagram extends GridPane {
return outputsBox;
}
private OutputNode buildPaymentNode(WalletTransaction.Output output) {
Payment payment = output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment();
boolean spConsolidation = output instanceof WalletTransaction.SilentPaymentConsolidationOutput;
Glyph outputGlyph = GlyphUtils.getOutputGlyph(walletTx, payment);
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon", "anchor-icon").contains(style)) || payment instanceof AdditionalPayment || payment.getLabel() != null;
boolean addressLabel = payment.getLabel() == null || payment.getType() == Payment.Type.MIX;
Label recipientLabel = new Label(payment.getType() == Payment.Type.FAKE_MIX ? payment.getType().toDisplayString() : (addressLabel ? payment.toString().substring(0, 8) + "..." : payment.getLabel()), outputGlyph);
recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
if(addressLabel) {
recipientLabel.setSkin(new AddressLabelSkin(recipientLabel));
}
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
Wallet toBip47Wallet = getBip47SendWallet(payment);
DnsPayment dnsPayment = DnsPaymentCache.getDnsPayment(payment);
Tooltip recipientTooltip = new Tooltip((toNode != null || spConsolidation ? "Consolidate " : (toWallet == null ? "Pay " : "Receive "))
+ getCoinValue(payment.getAmount()) + " to "
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toNode != null ? toNode : (spConsolidation ? walletTx.getWallet().getFullDisplayName() : (toWallet == null ? (dnsPayment == null ? (payment.getLabel() == null ? (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName()) : payment.getLabel()) : dnsPayment.toString()) : toWallet.getFullDisplayName()))) + "\n" + payment.getDisplayAddress())
+ (walletTx.isDuplicateAddress(payment) ? " (Duplicate)" : ""));
recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientTooltip.setShowDuration(Duration.INDEFINITE);
recipientTooltip.setWrapText(true);
recipientTooltip.setSkin(new AddressTooltipSkin(recipientTooltip));
Window activeWindow = AppServices.getActiveWindow();
if(activeWindow != null) {
recipientTooltip.setMaxWidth(activeWindow.getWidth());
}
recipientLabel.setTooltip(recipientTooltip);
HBox paymentBox = new HBox();
paymentBox.setAlignment(Pos.CENTER_LEFT);
paymentBox.getChildren().add(recipientLabel);
if(isExpanded()) {
recipientLabel.setMinWidth(120);
Region region = new Region();
region.setMinWidth(20);
HBox.setHgrow(region, Priority.ALWAYS);
CoinLabel amountLabel = new CoinLabel();
amountLabel.setValue(payment.getAmount());
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
paymentBox.getChildren().addAll(region, amountLabel);
}
if(payment instanceof SilentPayment silentPayment) {
return new OutputNode(paymentBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, payment.getAmount(), null, silentPayment.getSilentPaymentAddress());
}
Wallet bip47Wallet = toWallet != null && toWallet.isBip47() ? toWallet : (toBip47Wallet != null && toBip47Wallet.isBip47() ? toBip47Wallet : null);
PaymentCode paymentCode = bip47Wallet == null ? null : bip47Wallet.getKeystores().getFirst().getExternalPaymentCode();
return new OutputNode(paymentBox, payment.getAddress(), payment.getAmount(), paymentCode, null);
}
private OutputNode buildHdChangeNode(WalletTransaction.ChangeOutput changeOutput) {
WalletNode changeNode = changeOutput.getWalletNode();
long value = changeOutput.getValue();
boolean overGapLimit = walletTx.getWallet().getPolicyType() != PolicyType.SINGLE_SP &&
(changeNode.getIndex() - walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE).getIndex()) > walletTx.getWallet().getGapLimit();
HBox actionBox = new HBox();
actionBox.setAlignment(Pos.CENTER_LEFT);
Address changeAddress = walletTx.getChangeAddress(changeNode);
String changeDesc = changeAddress.toString().substring(0, 8) + "...";
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
changeLabel.getStyleClass().addAll("output-label", "change-label");
changeLabel.setSkin(new AddressLabelSkin(changeLabel));
Tooltip changeTooltip = new Tooltip("Change of " + getCoinValue(value) + " to " + changeNode + "\n" + changeAddress.toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
changeTooltip.getStyleClass().add("change-label");
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
changeTooltip.setShowDuration(Duration.INDEFINITE);
changeTooltip.setSkin(new AddressTooltipSkin(changeTooltip));
changeLabel.setTooltip(changeTooltip);
actionBox.getChildren().add(changeLabel);
if(!isFinal()) {
Button nextChangeAddressButton = new Button("");
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
nextChangeAddressButton.setOnAction(event -> {
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
});
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
replaceChangeLabel.getStyleClass().add("replace-change-label");
replaceChangeLabel.setVisible(false);
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
actionBox.getChildren().add(replaceChangeLabel);
}
if(isExpanded()) {
changeLabel.setMinWidth(120);
Region region = new Region();
region.setMinWidth(20);
HBox.setHgrow(region, Priority.ALWAYS);
CoinLabel amountLabel = new CoinLabel();
amountLabel.setValue(value);
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
actionBox.getChildren().addAll(region, amountLabel);
}
return new OutputNode(actionBox, changeAddress, value);
}
private OutputNode buildSpChangeNode(WalletTransaction.SilentPaymentChangeOutput spChangeOutput) {
SilentPayment silentPayment = spChangeOutput.getSilentPayment();
SilentPaymentAddress spAddress = silentPayment.getSilentPaymentAddress();
HBox actionBox = new HBox();
actionBox.setAlignment(Pos.CENTER_LEFT);
Label changeLabel = new Label("Change", getChangeGlyph());
changeLabel.getStyleClass().addAll("output-label", "payment-label");
changeLabel.setSkin(new AddressLabelSkin(changeLabel));
Tooltip changeTooltip = new Tooltip("Change of " + getCoinValue(silentPayment.getAmount()) + "\n" + silentPayment.getDisplayAddress());
changeTooltip.getStyleClass().add("change-label");
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
changeTooltip.setShowDuration(Duration.INDEFINITE);
changeTooltip.setSkin(new AddressTooltipSkin(changeTooltip));
changeLabel.setTooltip(changeTooltip);
actionBox.getChildren().add(changeLabel);
if(isExpanded()) {
changeLabel.setMinWidth(120);
Region region = new Region();
region.setMinWidth(20);
HBox.setHgrow(region, Priority.ALWAYS);
CoinLabel amountLabel = new CoinLabel();
amountLabel.setValue(silentPayment.getAmount());
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
actionBox.getChildren().addAll(region, amountLabel);
}
return new OutputNode(actionBox, silentPayment.isAddressComputed() ? silentPayment.getAddress() : null, silentPayment.getAmount(), null, spAddress);
}
private Pane getTransactionPane() {
VBox txPane = new VBox();
txPane.setPadding(new Insets(0, 5, 0, 5));
@ -1022,8 +970,8 @@ public class TransactionDiagram extends GridPane {
}
private String getDiagramTitle() {
if(!isFinal() && !walletTx.getPayments().isEmpty() && walletTx.getPayments().getFirst().getLabel() != null) {
return walletTx.getPayments().getFirst().getLabel();
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) + "]";
}
@ -1075,6 +1023,15 @@ public class TransactionDiagram extends GridPane {
return spacer;
}
private int getOutputIndex(Address address, long amount, Collection<Integer> seenIndexes) {
List<TransactionOutput> addressOutputs = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
.map(WalletTransaction.Output::getTransactionOutput).collect(Collectors.toList());
TransactionOutput output = addressOutputs.stream()
.filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount && !seenIndexes.contains(txOutput.getIndex()))
.findFirst().orElseThrow();
return addressOutputs.indexOf(output);
}
private Wallet getBip47SendWallet(Payment payment) {
if(walletTx.getWallet() != null) {
for(Wallet childWallet : walletTx.getWallet().getChildWallets()) {

View File

@ -96,38 +96,22 @@ public class TransactionDiagramLabel extends HBox {
outputLabels.add(remixOutputLabel);
}
} else {
List<OutputLabel> externalLabels = new ArrayList<>();
List<OutputLabel> consolidationLabels = new ArrayList<>();
List<OutputLabel> mixLabels = new ArrayList<>();
for(WalletTransaction.Output output : walletTx.getOutputs()) {
if(transactionDiagram.isPaymentAndNotChange(output)) {
Payment payment = output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment();
if(payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX) {
mixLabels.add(getOutputLabel(transactionDiagram, output));
} else if(payment.getType() == Payment.Type.DEFAULT || payment.getType() == Payment.Type.ANCHOR) {
if(output instanceof WalletTransaction.ConsolidationOutput || output instanceof WalletTransaction.SilentPaymentConsolidationOutput) {
consolidationLabels.add(getOutputLabel(transactionDiagram, output));
} else {
externalLabels.add(getOutputLabel(transactionDiagram, output));
}
}
}
}
List<Payment> payments = walletTx.getExternalPayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
List<OutputLabel> paymentLabels = payments.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList());
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
externalLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
}
outputLabels.addAll(externalLabels);
outputLabels.addAll(consolidationLabels);
outputLabels.addAll(mixLabels);
outputLabels.addAll(paymentLabels);
List<Payment> consolidations = walletTx.getWalletNodePayments().stream().filter(payment -> payment.getType() == Payment.Type.DEFAULT).collect(Collectors.toList());
outputLabels.addAll(consolidations.stream().map(consolidation -> getOutputLabel(transactionDiagram, consolidation)).collect(Collectors.toList()));
List<Payment> mixes = walletTx.getPayments().stream().filter(payment -> payment.getType() == Payment.Type.MIX || payment.getType() == Payment.Type.FAKE_MIX).collect(Collectors.toList());
outputLabels.addAll(mixes.stream().map(payment -> getOutputLabel(transactionDiagram, payment)).collect(Collectors.toList()));
}
for(WalletTransaction.Output output : walletTx.getOutputs()) {
if(output instanceof WalletTransaction.SilentPaymentChangeOutput spChange) {
outputLabels.add(getOutputLabel(transactionDiagram, spChange));
} else if(output instanceof WalletTransaction.ChangeOutput changeOutput) {
outputLabels.add(getOutputLabel(transactionDiagram, changeOutput));
}
}
Map<WalletNode, Long> changeMap = walletTx.getChangeMap();
outputLabels.addAll(changeMap.entrySet().stream().map(changeEntry -> getOutputLabel(transactionDiagram, changeEntry)).collect(Collectors.toList()));
OutputLabel feeOutputLabel = getFeeOutputLabel(transactionDiagram);
if(feeOutputLabel != null) {
@ -216,31 +200,22 @@ public class TransactionDiagramLabel extends HBox {
return getOutputLabel(glyph, text);
}
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, WalletTransaction.Output output) {
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Payment payment = output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment();
boolean spConsolidation = output instanceof WalletTransaction.SilentPaymentConsolidationOutput;
Wallet toWallet = walletTx.getToWallet(AppServices.get().getOpenWallets().keySet(), payment);
WalletNode toNode = payment instanceof WalletNodePayment walletNodePayment ? walletNodePayment.getWalletNode() : null;
Glyph glyph = GlyphUtils.getOutputGlyph(walletTx, payment);
String text = (toNode != null || spConsolidation ? "Consolidate " : (toWallet == null ? "Pay " : "Receive ")) + transactionDiagram.getCoinValue(payment.getAmount()) + " to " + payment;
Glyph glyph = GlyphUtils.getOutputGlyph(transactionDiagram.getWalletTransaction(), payment);
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getCoinValue(payment.getAmount()) + " to " + payment;
return getOutputLabel(glyph, text);
}
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, WalletTransaction.ChangeOutput changeOutput) {
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Map.Entry<WalletNode, Long> changeEntry) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Glyph glyph = GlyphUtils.getChangeGlyph();
String text = "Change of " + transactionDiagram.getCoinValue(changeOutput.getValue()) + " to " + walletTx.getChangeAddress(changeOutput.getWalletNode()).toString();
return getOutputLabel(glyph, text);
}
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, WalletTransaction.SilentPaymentChangeOutput spChangeOutput) {
Glyph glyph = GlyphUtils.getChangeGlyph();
String text = "Change of " + transactionDiagram.getCoinValue(spChangeOutput.getSilentPayment().getAmount()) + " to " + spChangeOutput.getSilentPayment();
String text = "Change of " + transactionDiagram.getCoinValue(changeEntry.getValue()) + " to " + walletTx.getChangeAddress(changeEntry.getKey()).toString();
return getOutputLabel(glyph, text);
}

View File

@ -37,8 +37,7 @@ public class UsbStatusButton extends MenuButton {
togglePassphraseService.setOnSucceeded(event1 -> {
EventManager.get().post(new RequestOpenWalletsEvent());
if(!device.getModel().externalPassphraseEntry()) {
AppServices.showAlertDialog("Restart device", "Reconnect your " + device.getModel().toDisplayString() + " to reset the passphrase." +
"\n\nIf it has a battery, hold down the power button until it restarts.", Alert.AlertType.INFORMATION);
AppServices.showAlertDialog("Reconnect device", "Reconnect your " + device.getModel().toDisplayString() + " to reset the passphrase.", Alert.AlertType.INFORMATION);
}
});
togglePassphraseService.setOnFailed(event1 -> {

View File

@ -44,13 +44,11 @@ public class WalletExportDialog extends Dialog<Wallet> {
AnchorPane.setRightAnchor(scrollPane, 0.0);
List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE_HD) {
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else if(wallet.getPolicyType() == PolicyType.MULTI_HD) {
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
exporters = List.of(new Descriptor(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
@ -49,13 +48,9 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
}
public WalletNameDialog(String initialName, boolean hasExistingTransactions, Date startDate, boolean rename) {
this(initialName, hasExistingTransactions, null, startDate, rename);
}
public WalletNameDialog(String initialName, boolean hasExistingTransactions, PolicyType walletPolicyType, Date startDate, boolean rename) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
boolean requestBirthDate = !rename && (walletPolicyType == PolicyType.SINGLE_SP || Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
boolean requestBirthDate = !rename && (Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
setTitle("Wallet Name");
dialogPane.setHeaderText("Enter a name for this wallet:");
@ -78,7 +73,7 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
name = (CustomTextField)TextFields.createClearableTextField();
name.setText(initialName);
name.setTextFormatter(new TextFormatter<>((change) -> {
change.setText(change.getText().replaceAll("[\\\\/:*?\"<>|;`]", "_"));
change.setText(change.getText().replaceAll("[\\\\/:*?\"<>|]", "_"));
return change;
}));
content.getChildren().add(name);

View File

@ -114,7 +114,7 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
private void importKeystore(List<ChildNumber> derivation) {
importButton.setDisable(true);
try {
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, xprv);
Keystore keystore = importer.getKeystore(derivation, xprv);
EventManager.get().post(new KeystoreImportEvent(keystore));
} catch (ImportException e) {
String errorMessage = e.getMessage();

View File

@ -20,6 +20,6 @@ public class SettingsChangedEvent {
}
public enum Type {
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, KEYSTORE_SP_SCAN, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
}
}

View File

@ -1,22 +0,0 @@
package com.sparrowwallet.sparrow.event;
/**
* Posted by SubscriptionService on a live silent-payments delta i.e. a progress = 1.0 notification
* that arrives after the historical scan has already completed. Carries the SP address only;
* consumers re-call ElectrumServer.getSilentPaymentHistory(scanAddress, neededStart) to get the
* full cache and apply their own wallet.getWalletTransaction(txid) filter for "what's new for me".
* <p>
* The first historical-scan-complete notification is consumed by the blocking getSilentPaymentHistory
* call's latch and does NOT post this event.
*/
public class SilentPaymentsHistoryUpdatedEvent {
private final String spAddress;
public SilentPaymentsHistoryUpdatedEvent(String spAddress) {
this.spAddress = spAddress;
}
public String getSpAddress() {
return spAddress;
}
}

View File

@ -1,24 +0,0 @@
package com.sparrowwallet.sparrow.event;
/**
* Posted by SubscriptionService on every blockchain.silentpayments.subscribe notification.
* Carries the SP address and the scan's progress (0.01.0). WalletForm consumers use this for
* status UI; the gating decision (whether to display) is per-wallet runtime state, not encoded here.
*/
public class SilentPaymentsScanProgressEvent {
private final String spAddress;
private final double progress;
public SilentPaymentsScanProgressEvent(String spAddress, double progress) {
this.spAddress = spAddress;
this.progress = progress;
}
public String getSpAddress() {
return spAddress;
}
public double getProgress() {
return progress;
}
}

View File

@ -1,21 +0,0 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
/**
* Posted by ElectrumServer.releaseSilentPaymentSubscription when the refcount on a silent-payments
* subscription reaches zero. The subscriber starts a background SilentPaymentsUnsubscribeService to
* issue the actual unsubscribe RPC, since releaseSilentPaymentSubscription is reached from JFX-thread
* paths (wallet close, refresh, history clear) where blocking on a network call is not acceptable.
*/
public class SilentPaymentsUnsubscribeEvent {
private final SilentPaymentScanAddress scanAddress;
public SilentPaymentsUnsubscribeEvent(SilentPaymentScanAddress scanAddress) {
this.scanAddress = scanAddress;
}
public SilentPaymentScanAddress getScanAddress() {
return scanAddress;
}
}

View File

@ -1,9 +0,0 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletSilentPaymentAddressesChangedEvent extends WalletChangedEvent {
public WalletSilentPaymentAddressesChangedEvent(Wallet wallet) {
super(wallet.resolveMasterWallet());
}
}

View File

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.glyphfont;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletNodePayment;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.control.TransactionDiagram;
@ -15,7 +16,7 @@ public class GlyphUtils {
return getFakeMixGlyph();
} else if(payment.getType().equals(Payment.Type.ANCHOR)) {
return getAnchorGlyph();
} else if(walletTx.isConsolidation(payment)) {
} else if(payment instanceof WalletNodePayment) {
return getConsolidationGlyph();
} else if(walletTx.isPremixSend(payment)) {
return getPremixGlyph();

View File

@ -4,7 +4,6 @@ import com.google.common.io.CharStreams;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.crypto.Pbkdf2KeyDeriver;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
@ -94,7 +93,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
if(password != null) {
@ -236,10 +235,6 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
}
String header = reader.readLine();
if(header == null || !header.startsWith("BSMS")) {
throw new ImportException("Not a BSMS file");
}
String descriptor = reader.readLine();
String paths = reader.readLine();
String address = reader.readLine();

View File

@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.MasterPrivateExtendedKey;
import com.sparrowwallet.drongo.wallet.WalletModel;
@ -26,10 +25,10 @@ public class Bip32 implements KeystoreXprvImport {
}
@Override
public Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException {
public Keystore getKeystore(List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException {
try {
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(xprv.getKey().getPrivKeyBytes(), xprv.getKey().getChainCode());
return Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, policyType, derivation);
return Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, derivation);
} catch(Exception e) {
throw new ImportException(e);
}

View File

@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.*;
import java.util.List;
@ -23,11 +22,11 @@ public class Bip39 implements KeystoreMnemonicImport {
}
@Override
public Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, List<String> mnemonicWords, String passphrase) throws ImportException {
public Keystore getKeystore(List<ChildNumber> derivation, List<String> mnemonicWords, String passphrase) throws ImportException {
try {
Bip39MnemonicCode.INSTANCE.check(mnemonicWords);
DeterministicSeed seed = new DeterministicSeed(mnemonicWords, passphrase, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
return Keystore.fromSeed(seed, policyType, derivation);
return Keystore.fromSeed(seed, derivation);
} catch (Exception e) {
try {
ElectrumMnemonicCode.INSTANCE.check(mnemonicWords);

View File

@ -3,7 +3,6 @@ package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.DeterministicKey;
import com.sparrowwallet.drongo.crypto.HDKeyDerivation;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.MasterPrivateExtendedKey;
import com.sparrowwallet.drongo.wallet.MnemonicException;
@ -29,12 +28,12 @@ public class Bip93 implements KeystoreCodexImport {
}
@Override
public Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, String secretShare) throws ImportException {
public Keystore getKeystore(List<ChildNumber> derivation, String secretShare) throws ImportException {
try {
Codex32.Codex32Data secretData = Codex32.decode(secretShare);
DeterministicKey key = HDKeyDerivation.createMasterPrivateKey(secretData.payloadToBip32Secret());
MasterPrivateExtendedKey mpek = new MasterPrivateExtendedKey(key);
Keystore keystore = Keystore.fromMasterPrivateExtendedKey(mpek, policyType, derivation);
Keystore keystore = Keystore.fromMasterPrivateExtendedKey(mpek, derivation);
keystore.setLabel("BIP93");
return keystore;
} catch(MnemonicException e) {

View File

@ -49,7 +49,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
Wallet wallet = new Wallet();
wallet.setName(cf.name);
wallet.setPolicyType(PolicyType.MULTI_HD);
wallet.setPolicyType(PolicyType.MULTI);
ScriptType scriptType = ScriptType.valueOf(cf.addressType.replace('-', '_'));
for(ExtPublicKey extKey : cf.extendedPublicKeys) {
@ -80,7 +80,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
}
wallet.setScriptType(scriptType);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI_HD, scriptType, wallet.getKeystores(), cf.quorum.requiredSigners));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), cf.quorum.requiredSigners));
return wallet;
} catch(Exception e) {
@ -99,7 +99,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
throw new ExportException("Cannot export an incomplete wallet");
}
if(!wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
if(!wallet.getPolicyType().equals(PolicyType.MULTI)) {
throw new ExportException(getName() + " import requires a multisig wallet");
}

View File

@ -5,7 +5,6 @@ import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.Keystore;
@ -108,7 +107,7 @@ public abstract class CardApi {
public abstract Service<Void> getInitializationService(byte[] entropy, StringProperty messageProperty);
public abstract Service<Keystore> getImportService(PolicyType policyType, List<ChildNumber> derivation, StringProperty messageProperty);
public abstract Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty);
public abstract Service<PSBT> getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty);

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
@ -20,9 +19,10 @@ public class CoboVaultMultisig extends ColdcardMultisig {
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
keystore.setLabel("Cobo Vault");
keystore.setWalletModel(getWalletModel());
return keystore;
}

View File

@ -35,11 +35,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
if(policyType == PolicyType.SINGLE_SP) {
throw new ImportException(getName() + " does not support receiving silent payments");
}
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
try {
Gson gson = new Gson();
CoboVaultSinglesigKeystore coboKeystore = gson.fromJson(new InputStreamReader(inputStream, StandardCharsets.UTF_8), CoboVaultSinglesigKeystore.class);
@ -51,7 +47,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
Keystore keystore = new Keystore();
keystore.setLabel(getName());
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(getWalletModel());
keystore.setWalletModel(WalletModel.COBO_VAULT);
keystore.setKeyDerivation(new KeyDerivation(coboKeystore.MasterFingerprint.toLowerCase(Locale.ROOT), "m/" + coboKeystore.AccountKeyPath, true));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(coboKeystore.ExtPubKey));
@ -74,13 +70,13 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
//Use default of P2WPKH
Keystore keystore = getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH, inputStream, "");
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE_HD);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(ScriptType.P2WPKH);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, ScriptType.P2WPKH, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
try {
wallet.checkWallet();

View File

@ -32,7 +32,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
inputStream.transferTo(baos);
@ -41,9 +41,9 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
Keystore keystore;
try {
keystore = getKeystoreMultisig(policyType, scriptType, firstClone, password);
keystore = getKeystoreMultisig(scriptType, firstClone, password);
} catch(Exception e) {
keystore = getKeystoreSinglesig(policyType, scriptType, secondClone, password);
keystore = getKeystoreSinglesig(scriptType, secondClone, password);
}
return keystore;
@ -52,18 +52,18 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
}
}
private Keystore getKeystoreSinglesig(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
private Keystore getKeystoreSinglesig(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
ColdcardSinglesig coldcardSinglesig = new ColdcardSinglesig();
return coldcardSinglesig.getKeystore(policyType, scriptType, inputStream, password);
return coldcardSinglesig.getKeystore(scriptType, inputStream, password);
}
public Keystore getKeystoreMultisig(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystoreMultisig(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
ColdcardKeystore cck = JsonPersistence.getGson().fromJson(reader, ColdcardKeystore.class);
Keystore keystore = new Keystore("Coldcard");
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(getWalletModel());
keystore.setWalletModel(WalletModel.COLDCARD);
try {
if(cck.xpub != null && cck.path != null) {
@ -119,7 +119,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.MULTI_HD);
wallet.setPolicyType(PolicyType.MULTI);
int threshold = 2;
ScriptType scriptType = ScriptType.P2SH;
@ -167,7 +167,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
}
Policy policy = Policy.getPolicy(PolicyType.MULTI_HD, scriptType, wallet.getKeystores(), threshold);
Policy policy = Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), threshold);
wallet.setDefaultPolicy(policy);
wallet.setScriptType(scriptType);
@ -194,7 +194,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
throw new ExportException("Cannot export an incomplete wallet");
}
if(!wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
if(!wallet.getPolicyType().equals(PolicyType.MULTI)) {
throw new ExportException(getName() + " import requires a multisig wallet");
}

View File

@ -8,7 +8,6 @@ import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
import com.sparrowwallet.drongo.wallet.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -55,37 +54,21 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
try {
Gson gson = new Gson();
Type stringStringMap = new TypeToken<Map<String, JsonElement>>() {
}.getType();
Map<String, JsonElement> map = gson.fromJson(new InputStreamReader(inputStream, StandardCharsets.UTF_8), stringStringMap);
if(map.get("xfp") == null) {
throw new ImportException("Export was not a valid " + getName() + " wallet export");
if (map.get("xfp") == null) {
throw new ImportException("File was not a valid " + getName() + " wallet export");
}
String masterFingerprint = map.get("xfp").getAsString();
if(policyType == PolicyType.SINGLE_SP) {
JsonElement bip352Element = map.get("bip352");
if(bip352Element == null) {
throw new ImportException("Export does not contain the spscan value for silent payments");
}
ColdcardKeystore ck = gson.fromJson(bip352Element, ColdcardKeystore.class);
Keystore keystore = new Keystore();
keystore.setLabel(getName());
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(getWalletModel());
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, ck.deriv, true));
keystore.setSilentPaymentScanAddress(SilentPaymentScanAddress.fromKeyString(ck.spscan));
return keystore;
}
for(String key : map.keySet()) {
if(key.startsWith("bip")) {
for (String key : map.keySet()) {
if (key.startsWith("bip")) {
ColdcardKeystore ck = gson.fromJson(map.get(key), ColdcardKeystore.class);
if(ck.name != null) {
@ -94,7 +77,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
Keystore keystore = new Keystore();
keystore.setLabel(getName());
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(getWalletModel());
keystore.setWalletModel(WalletModel.COLDCARD);
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, ck.deriv, true));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(ck.xpub));
@ -103,7 +86,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
}
}
}
} catch(Exception e) {
} catch (Exception e) {
throw new ImportException("Error getting " + getName() + " keystore", e);
}
@ -118,13 +101,13 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
//Use default of P2WPKH
Keystore keystore = getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH, inputStream, "");
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE_HD);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(ScriptType.P2WPKH);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, ScriptType.P2WPKH, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
try {
wallet.checkWallet();
@ -139,7 +122,6 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
public String deriv;
public String name;
public String xpub;
public String spscan;
public String xfp;
}
}

View File

@ -18,12 +18,12 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.lang.reflect.Type;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.ENUMERATE_HW_PERIOD_SECS;
import static com.sparrowwallet.sparrow.net.PagedBatchRequestBuilder.DEFAULT_PAGE_SIZE;
import static com.sparrowwallet.sparrow.net.TcpTransport.DEFAULT_MAX_TIMEOUT;
import static com.sparrowwallet.sparrow.wallet.WalletUtxosEntry.DUST_ATTACK_THRESHOLD_SATS;
import static com.sparrowwallet.sparrow.wallet.WalletUtxosEntry.DUST_ATTACK_THRESHOLD_SP_SATS;
public class Config {
private static final Logger log = LoggerFactory.getLogger(Config.class);
@ -59,12 +59,9 @@ public class Config {
private Boolean connectToBroadcast;
private Boolean connectToResolve;
private Boolean suggestSendToMany;
private Boolean suggestChangeWalletsDir;
private File walletsDir;
private List<File> recentWalletFiles;
private Integer keyDerivationPeriod;
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
private long dustAttackThresholdSp = DUST_ATTACK_THRESHOLD_SP_SATS;
private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS;
private QRDensity qrDensity;
private QREncoding qrEncoding;
@ -409,24 +406,6 @@ public class Config {
flush();
}
public Boolean getSuggestChangeWalletsDir() {
return suggestChangeWalletsDir;
}
public void setSuggestChangeWalletsDir(Boolean suggestChangeWalletsDir) {
this.suggestChangeWalletsDir = suggestChangeWalletsDir;
flush();
}
public File getWalletsDir() {
return walletsDir;
}
public void setWalletsDir(File walletsDir) {
this.walletsDir = walletsDir;
flush();
}
public List<File> getRecentWalletFiles() {
return recentWalletFiles;
}
@ -449,10 +428,6 @@ public class Config {
return dustAttackThreshold;
}
public long getDustAttackThresholdSp() {
return dustAttackThresholdSp;
}
public int getEnumerateHwPeriod() {
return enumerateHwPeriod;
}
@ -561,6 +536,13 @@ public class Config {
flush();
}
public void changePublicServer() {
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 Server getCoreServer() {
return coreServer;
}

View File

@ -3,10 +3,10 @@ package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.wallet.KeystoreController;
import java.io.*;
import java.nio.charset.StandardCharsets;
@ -29,43 +29,26 @@ public class Descriptor implements WalletImport, WalletExport {
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
bufferedWriter.write("# Receive and change descriptor (BIP389):");
bufferedWriter.newLine();
bufferedWriter.write("# Single argument descriptor:");
bufferedWriter.newLine();
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null);
bufferedWriter.write(outputDescriptor.toString(true));
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.write(outputDescriptor.toString(true));
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.write("# Two argument descriptor:");
bufferedWriter.newLine();
bufferedWriter.write(outputDescriptor.toString(true, true, true, true));
bufferedWriter.newLine();
} else {
bufferedWriter.write("# Receive and change descriptor:");
bufferedWriter.newLine();
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null);
bufferedWriter.write(outputDescriptor.toString(true));
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.write("# Receive descriptor:");
bufferedWriter.newLine();
OutputDescriptor receiveDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.RECEIVE, null);
bufferedWriter.write(receiveDescriptor.toString(true));
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.write("# Change descriptor:");
bufferedWriter.newLine();
OutputDescriptor changeDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.CHANGE, null);
bufferedWriter.write(changeDescriptor.toString(true));
bufferedWriter.newLine();
}
bufferedWriter.write("# Receive descriptor (Bitcoin Core):");
bufferedWriter.newLine();
OutputDescriptor receiveDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.RECEIVE, null);
bufferedWriter.write(receiveDescriptor.toString(true));
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.write("# Change descriptor (Bitcoin Core):");
bufferedWriter.newLine();
OutputDescriptor changeDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.CHANGE, null);
bufferedWriter.write(changeDescriptor.toString(true));
bufferedWriter.newLine();
bufferedWriter.flush();
} catch(Exception e) {
@ -131,14 +114,26 @@ public class Descriptor implements WalletImport, WalletExport {
private static List<String> getParagraphs(InputStream inputStream) {
List<String> paragraphs = new ArrayList<>();
StringBuilder paragraph = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
for(String line : reader.lines().map(String::trim).toArray(String[]::new)) {
if(!line.isEmpty() && !line.startsWith("#")) {
paragraphs.add(line.replaceFirst("^.+:", "").trim());
if(line.isEmpty()) {
if(!paragraph.isEmpty()) {
paragraphs.add(paragraph.toString());
paragraph.setLength(0);
}
} else if(line.startsWith("#")) {
continue;
} else {
paragraph.append(line.replaceFirst("^.+:", "").trim());
}
}
if(!paragraph.isEmpty()) {
paragraphs.add(paragraph.toString());
}
return paragraphs;
}

View File

@ -40,10 +40,10 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Wallet wallet = importWallet(inputStream, password);
if(wallet.getPolicyType().equals(PolicyType.MULTI_HD) || wallet.getKeystores().size() != 1) {
if(!wallet.getPolicyType().equals(PolicyType.SINGLE) || wallet.getKeystores().size() != 1) {
throw new ImportException("Multisig wallet detected - import it using File > Import Wallet");
}
@ -203,13 +203,13 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
wallet.setScriptType(scriptType);
if(ew.wallet_type.equals("standard")) {
wallet.setPolicyType(PolicyType.SINGLE_HD);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, scriptType, wallet.getKeystores(), 1));
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
} else if(ew.wallet_type.contains("of")) {
wallet.setPolicyType(PolicyType.MULTI_HD);
wallet.setPolicyType(PolicyType.MULTI);
String[] mOfn = ew.wallet_type.split("of");
int threshold = Integer.parseInt(mOfn[0]);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI_HD, scriptType, wallet.getKeystores(), threshold));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), threshold));
} else {
throw new ImportException("Unknown Electrum wallet type of " + ew.wallet_type);
}
@ -308,9 +308,9 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
try {
ElectrumJsonWallet ew = new ElectrumJsonWallet();
if(wallet.getPolicyType().equals(PolicyType.SINGLE_HD)) {
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
ew.wallet_type = "standard";
} else if(wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
} else if(wallet.getPolicyType().equals(PolicyType.MULTI)) {
ew.wallet_type = wallet.getDefaultPolicy().getNumSignaturesRequired() + "of" + wallet.getKeystores().size();
} else {
throw new ExportException("Could not export a wallet with a " + wallet.getPolicyType() + " policy");
@ -367,9 +367,9 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
throw new ExportException("Cannot export a keystore of source " + keystore.getSource());
}
if(wallet.getPolicyType().equals(PolicyType.SINGLE_HD)) {
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
ew.keystores.put("keystore", ek);
} else if(wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
} else if(wallet.getPolicyType().equals(PolicyType.MULTI)) {
ew.keystores.put("x" + index + "/", ek);
}

View File

@ -30,10 +30,6 @@ public class ElectrumPersonalServer implements WalletExport {
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
throw new ExportException(getName() + " does not support silent payments wallets.");
}
if(wallet.getScriptType() == ScriptType.P2TR) {
throw new ExportException(getName() + " does not support Taproot wallets.");
}
@ -65,7 +61,7 @@ public class ElectrumPersonalServer implements WalletExport {
writer.write(wallet.getFullName().replace(' ', '_') + " = ");
ExtendedKey.Header xpubHeader = ExtendedKey.Header.fromScriptType(wallet.getScriptType(), false);
if(wallet.getPolicyType() == PolicyType.MULTI_HD) {
if(wallet.getPolicyType() == PolicyType.MULTI) {
writer.write(wallet.getDefaultPolicy().getNumSignaturesRequired() + " ");
}

View File

@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
@ -26,7 +25,7 @@ public class GordianSeedTool implements KeystoreFileImport {
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
throw new ImportException("Only QR imports are supported.");
}

View File

@ -6,7 +6,6 @@ import com.sparrowwallet.drongo.OsType;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.lark.DeviceException;
@ -146,7 +145,7 @@ public class Hwi {
}
}
public Map<WalletType, ExtendedKey> getXpubs(Device device, String passphrase, Map<WalletType, String> accountDerivationPaths, Map<WalletType, ExtendedKey> accountXpubs) throws ImportException {
public Map<WalletType, String> getXpubs(Device device, String passphrase, Map<WalletType, String> accountDerivationPaths, Map<WalletType, String> accountXpubs) throws ImportException {
for(Map.Entry<WalletType, String> entry : accountDerivationPaths.entrySet()) {
accountXpubs.put(entry.getKey(), getXpub(device, passphrase, entry.getValue()));
}
@ -154,12 +153,12 @@ public class Hwi {
return accountXpubs;
}
public ExtendedKey getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
try {
Lark lark = getLark(passphrase);
ExtendedKey xpub = lark.getPubKeyAtPath(device.getType(), device.getPath(), derivationPath);
isPromptActive = false;
return xpub;
return xpub.toString();
} catch(DeviceException e) {
throw new ImportException(e.getMessage(), e);
} catch(RuntimeException e) {
@ -168,20 +167,6 @@ public class Hwi {
}
}
public SilentPaymentScanAddress getSpscan(Device device, String passphrase, String derivationPath) throws ImportException {
try {
Lark lark = getLark(passphrase);
SilentPaymentScanAddress spscan = lark.getSpscanAtPath(device.getType(), device.getPath(), derivationPath);
isPromptActive = false;
return spscan;
} catch(DeviceException e) {
throw new ImportException(e.getMessage(), e);
} catch(RuntimeException e) {
log.error("Error retrieving spscan", e);
throw e;
}
}
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor,
OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) throws DisplayAddressException {
try {
@ -436,7 +421,7 @@ public class Hwi {
}
}
public static class GetXpubService extends Service<ExtendedKey> {
public static class GetXpubService extends Service<String> {
private final Device device;
private final String passphrase;
private final String derivationPath;
@ -448,9 +433,9 @@ public class Hwi {
}
@Override
protected Task<ExtendedKey> createTask() {
protected Task<String> createTask() {
return new Task<>() {
protected ExtendedKey call() throws ImportException {
protected String call() throws ImportException {
Hwi hwi = new Hwi();
return hwi.getXpub(device, passphrase, derivationPath);
}
@ -458,29 +443,7 @@ public class Hwi {
}
}
public static class GetSpscanService extends Service<SilentPaymentScanAddress> {
private final Device device;
private final String passphrase;
private final String derivationPath;
public GetSpscanService(Device device, String passphrase, String derivationPath) {
this.device = device;
this.passphrase = passphrase;
this.derivationPath = derivationPath;
}
@Override
protected Task<SilentPaymentScanAddress> createTask() {
return new Task<>() {
protected SilentPaymentScanAddress call() throws ImportException {
Hwi hwi = new Hwi();
return hwi.getSpscan(device, passphrase, derivationPath);
}
};
}
}
public static class GetXpubsService extends Service<Map<WalletType, ExtendedKey>> {
public static class GetXpubsService extends Service<Map<WalletType, String>> {
private final Device device;
private final String passphrase;
private final Map<WalletType, String> accountDerivationPaths;
@ -492,13 +455,13 @@ public class Hwi {
}
@Override
protected Task<Map<WalletType, ExtendedKey>> createTask() {
protected Task<Map<WalletType, String>> createTask() {
return new Task<>() {
protected Map<WalletType, ExtendedKey> call() throws ImportException {
protected Map<WalletType, String> call() throws ImportException {
Hwi hwi = new Hwi();
updateProgress(0, accountDerivationPaths.size());
ObservableMap<WalletType, ExtendedKey> accountXpubs = FXCollections.observableMap(new LinkedHashMap<>());
accountXpubs.addListener((MapChangeListener<? super WalletType, ? super ExtendedKey>) _ -> updateProgress(accountXpubs.size(), accountDerivationPaths.size()));
ObservableMap<WalletType, String> accountXpubs = FXCollections.observableMap(new LinkedHashMap<>());
accountXpubs.addListener((MapChangeListener<? super WalletType, ? super String>) _ -> updateProgress(accountXpubs.size(), accountDerivationPaths.size()));
return hwi.getXpubs(device, passphrase, accountDerivationPaths, accountXpubs);
}
};

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
@ -25,7 +24,7 @@ public class Jade implements KeystoreFileImport {
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
throw new ImportException("Failed to detect a valid " + scriptType.getDescription() + " keystore.");
}

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
@ -64,13 +63,14 @@ public class JadeMultisig extends ColdcardMultisig {
Wallet wallet = super.importWallet(inputStream, password);
for(Keystore keystore : wallet.getKeystores()) {
keystore.setLabel(keystore.getLabel().replace("Coldcard", "Jade"));
keystore.setWalletModel(WalletModel.JADE);
}
return wallet;
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
throw new ImportException("Failed to detect a valid " + scriptType.getDescription() + " keystore.");
}

View File

@ -12,7 +12,6 @@ import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
@ -338,8 +337,6 @@ public class JsonPersistence implements Persistence {
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionDeserializer());
gsonBuilder.registerTypeAdapter(Address.class, new AddressSerializer());
gsonBuilder.registerTypeAdapter(Address.class, new AddressDeserializer());
gsonBuilder.registerTypeAdapter(PolicyType.class, new PolicyTypeSerializer());
gsonBuilder.registerTypeAdapter(PolicyType.class, new PolicyTypeDeserializer());
if(includeWalletSerializers) {
gsonBuilder.registerTypeAdapter(Keystore.class, new KeystoreSerializer());
gsonBuilder.registerTypeAdapter(WalletNode.class, new NodeSerializer());
@ -456,30 +453,6 @@ public class JsonPersistence implements Persistence {
}
}
private static class PolicyTypeSerializer implements JsonSerializer<PolicyType> {
@Override
public JsonElement serialize(PolicyType src, Type typeOfSrc, JsonSerializationContext context) {
return switch(src) {
case SINGLE_HD -> new JsonPrimitive("SINGLE");
case MULTI_HD -> new JsonPrimitive("MULTI");
default -> new JsonPrimitive(src.name());
};
}
}
private static class PolicyTypeDeserializer implements JsonDeserializer<PolicyType> {
@Override
public PolicyType deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
String value = json.getAsJsonPrimitive().getAsString();
return switch(value) {
case "SINGLE" -> PolicyType.SINGLE_HD;
case "MULTI" -> PolicyType.MULTI_HD;
default -> PolicyType.valueOf(value);
};
}
}
private static class KeystoreSerializer implements JsonSerializer<Keystore> {
@Override
public JsonElement serialize(Keystore keystore, Type typeOfSrc, JsonSerializationContext context) {

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
@ -24,9 +23,10 @@ public class KeycardShellMultisig extends ColdcardMultisig {
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
keystore.setLabel("Keycard Shell");
keystore.setWalletModel(getWalletModel());
return keystore;
}

View File

@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
@ -25,9 +24,10 @@ public class KeycardShellSinglesig extends KeystoneSinglesig {
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
keystore.setLabel("Keycard Shell");
keystore.setWalletModel(getWalletModel());
return keystore;
}

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
@ -20,9 +19,10 @@ public class KeystoneMultisig extends ColdcardMultisig {
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
keystore.setLabel("Keystone");
keystore.setWalletModel(getWalletModel());
return keystore;
}

View File

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.io;
import com.google.gson.Gson;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
@ -36,7 +37,7 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
try {
String outputDescriptor = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n"));
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(outputDescriptor);
@ -45,22 +46,24 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
throw new IllegalArgumentException("Output descriptor describes a multisig wallet");
}
if(policyType == PolicyType.SINGLE_SP && !descriptor.isSilentPayments()) {
throw new IllegalArgumentException("Export does not contain the spscan value for silent payments");
}
if(descriptor.getScriptType() != scriptType) {
throw new IllegalArgumentException("Output descriptor describes a " + descriptor.getScriptType().getDescription() + " wallet");
}
Wallet wallet = descriptor.toWallet();
Keystore keystore = wallet.getKeystores().getFirst();
ExtendedKey xpub = descriptor.getSingletonExtendedPublicKey();
KeyDerivation keyDerivation = descriptor.getKeyDerivation(xpub);
Keystore keystore = new Keystore();
keystore.setLabel(getName());
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(getWalletModel());
keystore.setWalletModel(WalletModel.KEYSTONE);
keystore.setKeyDerivation(keyDerivation);
keystore.setExtendedPublicKey(xpub);
return keystore;
} catch(Exception e) {
} catch (IllegalArgumentException e) {
throw new ImportException("Error getting " + getName() + " keystore - not an output descriptor", e);
} catch (Exception e) {
throw new ImportException("Error getting " + getName() + " keystore", e);
}
}
@ -73,13 +76,13 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
//Use default of P2WPKH
Keystore keystore = getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH, inputStream, "");
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE_HD);
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setScriptType(ScriptType.P2WPKH);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, ScriptType.P2WPKH, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
try {
wallet.checkWallet();

View File

@ -1,13 +1,12 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Keystore;
import javafx.beans.property.StringProperty;
import java.util.List;
public interface KeystoreCardImport extends CardImport {
Keystore getKeystore(PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException;
Keystore getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException;
String getKeystoreImportDescription(int account);
}

View File

@ -1,11 +1,10 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Keystore;
import java.util.List;
public interface KeystoreCodexImport extends KeystoreImport {
Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, String secretShare) throws ImportException;
Keystore getKeystore(List<ChildNumber> derivation, String secretShare) throws ImportException;
}

View File

@ -1,13 +1,12 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import java.io.InputStream;
public interface KeystoreFileImport extends KeystoreImport, FileImport {
Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
String getKeystoreImportDescription(int account);
default String getKeystoreImportDescription() {
return getKeystoreImportDescription(0);

View File

@ -1,11 +1,10 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Keystore;
import java.util.List;
public interface KeystoreMnemonicImport extends KeystoreImport {
Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, List<String> mnemonicWords, String passphrase) throws ImportException;
Keystore getKeystore(List<ChildNumber> derivation, List<String> mnemonicWords, String passphrase) throws ImportException;
}

View File

@ -1,11 +1,10 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Keystore;
import java.util.List;
public interface KeystoreMnemonicShareImport extends KeystoreImport {
Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException;
Keystore getKeystore(List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException;
}

View File

@ -2,11 +2,10 @@ package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Keystore;
import java.util.List;
public interface KeystoreXprvImport extends KeystoreImport {
Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException;
Keystore getKeystore(List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException;
}

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.Wallet;
@ -20,9 +19,10 @@ public class PassportMultisig extends ColdcardMultisig {
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
keystore.setLabel("Passport");
keystore.setWalletModel(getWalletModel());
return keystore;
}

View File

@ -1,6 +1,5 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
@ -19,9 +18,10 @@ public class PassportSinglesig extends ColdcardSinglesig {
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
keystore.setLabel("Passport");
keystore.setWalletModel(getWalletModel());
return keystore;
}

View File

@ -7,7 +7,6 @@ import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.SamouraiUtil;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.*;
@ -26,15 +25,14 @@ public class Samourai implements KeystoreFileImport {
}
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
if(policyType == PolicyType.SINGLE_SP) {
throw new ImportException(getName() + " does not support receiving silent payments");
}
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
try {
String input = CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
Map<String, JsonElement> map = this.parseJsonInput(input);
Gson gson = new Gson();
Type stringStringMap = new TypeToken<Map<String, JsonElement>>() {
}.getType();
Map<String, JsonElement> map = gson.fromJson(input, stringStringMap);
String payload = input;
if(map.containsKey("payload")) {
@ -55,9 +53,9 @@ public class Samourai implements KeystoreFileImport {
throw new ImportException("Unsupported backup version: " + version);
}
SamouraiBackup backup = new Gson().fromJson(decrypted, SamouraiBackup.class);
SamouraiBackup backup = gson.fromJson(decrypted, SamouraiBackup.class);
DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes(backup.wallet.seed), password, 0);
Keystore keystore = Keystore.fromSeed(seed, PolicyType.SINGLE_HD, scriptType.getDefaultDerivation());
Keystore keystore = Keystore.fromSeed(seed, scriptType.getDefaultDerivation());
keystore.setLabel(getWalletModel().toDisplayString());
return keystore;
} catch(JsonParseException e) {
@ -69,24 +67,6 @@ public class Samourai implements KeystoreFileImport {
}
}
private Map<String, JsonElement> parseJsonInput(String input) {
Gson gson = new Gson();
Type stringStringMap = new TypeToken<Map<String, JsonElement>>() {
}.getType();
try {
return gson.fromJson(input, stringStringMap);
} catch (JsonParseException e) {
int closingBracket = input.indexOf('}');
if (closingBracket < 0) {
throw e;
}
String fixedInput = input.substring(0, closingBracket + 1);
return gson.fromJson(fixedInput, stringStringMap);
}
}
@Override
public boolean isKeystoreImportScannable() {
return false;

View File

@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.drongo.wallet.slip39.RecoveryState;
import com.sparrowwallet.drongo.wallet.slip39.Share;
@ -26,7 +25,7 @@ public class Slip39 implements KeystoreMnemonicShareImport {
}
@Override
public Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException {
public Keystore getKeystore(List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException {
try {
RecoveryState recoveryState = new RecoveryState();
for(List<String> mnemonicWords : mnemonicShares) {
@ -37,7 +36,7 @@ public class Slip39 implements KeystoreMnemonicShareImport {
if(recoveryState.isComplete()) {
byte[] secret = recoveryState.recover(passphrase.getBytes(StandardCharsets.UTF_8));
DeterministicSeed seed = new DeterministicSeed(secret, passphrase, System.currentTimeMillis(), DeterministicSeed.Type.SLIP39);
return Keystore.fromSeed(seed, policyType, derivation);
return Keystore.fromSeed(seed, derivation);
} else {
throw new Slip39ProgressException(recoveryState.getShortStatus(), recoveryState.getStatus());
}

View File

@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.io;
import com.google.common.io.CharStreams;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
@ -18,24 +17,24 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
private static final Logger log = LoggerFactory.getLogger(SpecterDIY.class);
@Override
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
try {
String text = CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String outputDesc = policyType == PolicyType.SINGLE_SP ? "sp(" + text + ")" : "sh(" + text + ")";
String outputDesc = "sh(" + text + ")";
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(outputDesc);
Wallet wallet = outputDescriptor.toWallet();
if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Could not determine keystore from import");
throw new ImportException("Could not determine keystore from import");
}
Keystore keystore = wallet.getKeystores().getFirst();
Keystore keystore = wallet.getKeystores().get(0);
keystore.setLabel(getName());
keystore.setWalletModel(getWalletModel());
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
return keystore;
} catch(Exception e) {
} catch(IOException e) {
throw new ImportException("Error getting " + getName() + " keystore", e);
}
}

View File

@ -2,13 +2,11 @@ package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.PolicyType;
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.net.ServerType;
import com.sparrowwallet.sparrow.SparrowWallet;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
@ -183,20 +181,18 @@ public class Storage {
Keystore keystore = wallet.getKeystores().get(i);
if(keystore.hasSeed()) {
Keystore copyKeystore = copy.getKeystores().get(i);
Keystore derivedKeystore = Keystore.fromSeed(copyKeystore.getSeed(), wallet.getPolicyType(), copyKeystore.getKeyDerivation().getDerivation());
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());
keystore.setSilentPaymentScanAddress(derivedKeystore.getSilentPaymentScanAddress());
copyKeystore.getSeed().clear();
} else if(keystore.hasMasterPrivateExtendedKey()) {
Keystore copyKeystore = copy.getKeystores().get(i);
Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), wallet.getPolicyType(), copyKeystore.getKeyDerivation().getDerivation());
Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), copyKeystore.getKeyDerivation().getDerivation());
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
keystore.setSilentPaymentScanAddress(derivedKeystore.getSilentPaymentScanAddress());
copyKeystore.getMasterPrivateKey().clear();
}
}
@ -489,16 +485,7 @@ public class Storage {
}
public static File getWalletsDir() {
File walletsDir = Config.get().getWalletsDir();
if(walletsDir != null) {
if(!walletsDir.exists() && (walletsDir.getParentFile() == null || !walletsDir.getParentFile().exists() || !walletsDir.getParentFile().canWrite())) {
log.info("Configured wallets directory " + walletsDir.getAbsolutePath() + " is not reachable, reverting to default");
walletsDir = null;
}
}
if(walletsDir == null) {
walletsDir = new File(getSparrowDir(), WALLETS_DIR);
}
File walletsDir = new File(getSparrowDir(), WALLETS_DIR);
if(!walletsDir.exists()) {
createOwnerOnlyDirectory(walletsDir);
}
@ -507,23 +494,8 @@ public class Storage {
}
public static File getCertificateFile(String host) {
return findCertFile(getCertName(host));
}
public static void saveCertificate(String host, Certificate cert) {
writeCertPem(getCertName(host), cert);
}
public static File getCaCertificateFile(String host) {
return findCertFile(host + ".cacert");
}
public static void saveCaCertificate(String host, Certificate cert) {
writeCertPem(host + ".cacert", cert);
}
private static File findCertFile(String filename) {
File[] certs = getCertsDir().listFiles((dir, name) -> name.equals(filename));
File certsDir = getCertsDir();
File[] certs = certsDir.listFiles((dir, name) -> name.equals(host));
if(certs != null && certs.length > 0) {
return certs[0];
}
@ -531,8 +503,8 @@ public class Storage {
return null;
}
private static void writeCertPem(String filename, Certificate cert) {
try(FileWriter writer = new FileWriter(new File(getCertsDir(), filename))) {
public static void saveCertificate(String host, Certificate cert) {
try(FileWriter writer = new FileWriter(new File(getCertsDir(), host))) {
writer.write("-----BEGIN CERTIFICATE-----\n");
writer.write(Base64.getEncoder().encodeToString(cert.getEncoded()).replaceAll("(.{64})", "$1\n"));
writer.write("\n-----END CERTIFICATE-----\n");
@ -543,14 +515,6 @@ public class Storage {
}
}
private static String getCertName(String host) {
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
return host + ".bitcoind";
}
return host;
}
static File getCertsDir() {
File certsDir = new File(getSparrowDir(), CERTS_DIR);
if(!certsDir.exists()) {

View File

@ -6,7 +6,6 @@ import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
@ -69,11 +68,7 @@ public class WalletLabels implements WalletImport, WalletExport {
for(Keystore keystore : exportWallet.getKeystores()) {
if(keystore.getLabel() != null && !keystore.getLabel().isEmpty()) {
if(exportWallet.getPolicyType() == PolicyType.SINGLE_SP && keystore.getSilentPaymentScanAddress() != null) {
labels.add(new Label(Type.spscan, keystore.getSilentPaymentScanAddress().toKeyString(), keystore.getLabel(), null, null));
} else if(keystore.getExtendedPublicKey() != null) {
labels.add(new Label(Type.xpub, keystore.getExtendedPublicKey().toString(), keystore.getLabel(), null, null));
}
labels.add(new Label(Type.xpub, keystore.getExtendedPublicKey().toString(), keystore.getLabel(), null, null));
}
}
@ -225,17 +220,7 @@ public class WalletLabels implements WalletImport, WalletExport {
if(label.type == Type.xpub) {
for(Keystore keystore : wallet.getKeystores()) {
if(keystore.getExtendedPublicKey() != null && keystore.getExtendedPublicKey().toString().equals(label.ref)) {
keystore.setLabel(label.label);
List<Keystore> changedKeystores = changedWalletKeystores.computeIfAbsent(wallet, w -> new ArrayList<>());
changedKeystores.add(keystore);
}
}
}
if(label.type == Type.spscan) {
for(Keystore keystore : wallet.getKeystores()) {
if(keystore.getSilentPaymentScanAddress() != null && keystore.getSilentPaymentScanAddress().toKeyString().equals(label.ref)) {
if(keystore.getExtendedPublicKey().toString().equals(label.ref)) {
keystore.setLabel(label.label);
List<Keystore> changedKeystores = changedWalletKeystores.computeIfAbsent(wallet, w -> new ArrayList<>());
changedKeystores.add(keystore);
@ -447,7 +432,7 @@ public class WalletLabels implements WalletImport, WalletExport {
}
private enum Type {
tx, addr, pubkey, input, output, xpub, spscan
tx, addr, pubkey, input, output, xpub
}
private static class Label {

View File

@ -10,36 +10,15 @@ import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.io.File;
public class ZBar {
private static final Logger log = LoggerFactory.getLogger(ZBar.class);
private static boolean zbarLoaded;
public static boolean isEnabled() {
return com.sparrowwallet.sparrow.io.Config.get().isUseZbar();
}
private static synchronized void loadZBar() {
if(!zbarLoaded) {
String javaHome = System.getProperty("java.home");
if(javaHome != null) {
File libDir = new File(javaHome, "lib");
File iconvFile = new File(libDir, "iconv-2.dll");
if(iconvFile.exists()) {
System.load(iconvFile.getAbsolutePath());
}
File libFile = new File(libDir, System.mapLibraryName("zbar"));
if(libFile.exists()) {
System.load(libFile.getAbsolutePath());
}
}
zbarLoaded = true;
}
}
public static Scan scan(BufferedImage bufferedImage) {
loadZBar();
try {
BufferedImage grayscale = new BufferedImage(bufferedImage.getWidth(), bufferedImage.getHeight(), BufferedImage.TYPE_BYTE_GRAY);
Graphics2D g2d = (Graphics2D)grayscale.getGraphics();

View File

@ -6,7 +6,6 @@ import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
@ -142,12 +141,12 @@ public class CkCardApi extends CardApi {
}
@Override
public Service<Keystore> getImportService(PolicyType policyType, List<ChildNumber> derivation, StringProperty messageProperty) {
public Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty) {
if(cardType == WalletModel.SATSCHIP) {
return new CardImportPane.CardImportService(new Satschip(), policyType, cvc, derivation, messageProperty);
return new CardImportPane.CardImportService(new Satschip(), cvc, derivation, messageProperty);
}
return new CardImportPane.CardImportService(new Tapsigner(), policyType, cvc, derivation, messageProperty);
return new CardImportPane.CardImportService(new Tapsigner(), cvc, derivation, messageProperty);
}
@Override
@ -301,7 +300,7 @@ public class CkCardApi extends CardApi {
}
CardRead cardRead = cardProtocol.read(null, currentSlot);
Address address = getDefaultScriptType().getAddress(PolicyType.SINGLE_HD, cardRead.getPubKey());
Address address = getDefaultScriptType().getAddress(cardRead.getPubKey());
String left = addr.substring(0, addr.indexOf('_'));
String right = addr.substring(addr.lastIndexOf('_') + 1);

View File

@ -1,7 +1,6 @@
package com.sparrowwallet.sparrow.io.ckcard;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.sparrow.io.KeystoreCardImport;
@ -53,11 +52,7 @@ public class Tapsigner implements KeystoreCardImport {
}
@Override
public Keystore getKeystore(PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
if(policyType == PolicyType.SINGLE_SP) {
throw new ImportException(getName() + " does not support receiving silent payments");
}
public Keystore getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
if(pin.length() < 6) {
throw new ImportException("PIN too short.");
}

View File

@ -36,7 +36,6 @@ import java.nio.file.StandardCopyOption;
import java.security.SecureRandom;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@ -55,7 +54,6 @@ public class DbPersistence implements Persistence {
private static final String H2_USER = "sa";
private static final String H2_PASSWORD = "";
public static final String MIGRATION_RESOURCES_DIR = "com/sparrowwallet/sparrow/sql/";
private static final Pattern JDBC_URL_INJECTION_PATTERN = Pattern.compile(";\\w+=");
private HikariDataSource dataSource;
private AsymmetricKeyDeriver keyDeriver;
@ -83,7 +81,6 @@ public class DbPersistence implements Persistence {
ECKey encryptionKey = getEncryptionKey(password, storage.getWalletFile(), alreadyDerivedKey);
migrate(storage, MASTER_SCHEMA, encryptionKey);
validateSchema(storage, MASTER_SCHEMA, encryptionKey);
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
masterWallet = jdbi.withHandle(handle -> {
@ -113,7 +110,6 @@ public class DbPersistence implements Persistence {
Map<WalletAndKey, Storage> childWallets = new TreeMap<>();
for(String schema : childSchemas) {
migrate(storage, schema, encryptionKey);
validateSchema(storage, schema, encryptionKey);
Jdbi childJdbi = getJdbi(storage, getFilePassword(encryptionKey));
Wallet wallet = childJdbi.withHandle(handle -> {
@ -162,20 +158,11 @@ public class DbPersistence implements Persistence {
@Override
public void updateWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws StorageException {
String newPassword = getFilePassword(encryptionPubKey);
String currentPassword = getDatasourcePassword();
updatePassword(storage, encryptionPubKey);
updateExecutor.execute(() -> {
try {
if(dataSource != null && currentPassword != null && newPassword == null) {
//Removing encryption: write data first
update(storage, wallet, currentPassword);
updatePassword(storage, encryptionPubKey);
} else {
//Adding encryption or no change: change file first
updatePassword(storage, encryptionPubKey);
update(storage, wallet, newPassword);
}
update(storage, wallet, getFilePassword(encryptionPubKey));
} catch(Exception e) {
log.error("Error updating wallet db", e);
}
@ -226,7 +213,7 @@ public class DbPersistence implements Persistence {
WalletDao walletDao = handle.attach(WalletDao.class);
try {
if(dirtyPersistables.deleteAccount && !wallet.isMasterWallet()) {
handle.execute("drop schema `" + getSchema(wallet).replace("`", "``") + "` cascade");
handle.execute("drop schema `" + getSchema(wallet) + "` cascade");
return;
}
@ -249,11 +236,11 @@ public class DbPersistence implements Persistence {
if(addressNode.getId() == null) {
WalletNode purposeNode = wallet.getNode(addressNode.getKeyPurpose());
if(purposeNode.getId() == null) {
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null, null, null);
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null, null);
purposeNode.setId(purposeNodeId);
}
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId(), addressNode.getAddressData(), addressNode.getSilentPaymentTweak());
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId(), addressNode.getAddressData());
addressNode.setId(nodeId);
} else if(addressNode.getAddress() != null) {
walletNodeDao.updateNodeAddressData(addressNode.getId(), addressNode.getAddressData());
@ -308,11 +295,11 @@ public class DbPersistence implements Persistence {
if(addressNode.getId() == null) {
WalletNode purposeNode = wallet.getNode(addressNode.getKeyPurpose());
if(purposeNode.getId() == null) {
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null, null, null);
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null, null);
purposeNode.setId(purposeNodeId);
}
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId(), addressNode.getAddressData(), addressNode.getSilentPaymentTweak());
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId(), addressNode.getAddressData());
addressNode.setId(nodeId);
} else if(addressNode.getAddress() != null) {
walletNodeDao.updateNodeAddressData(addressNode.getId(), addressNode.getAddressData());
@ -337,11 +324,6 @@ public class DbPersistence implements Persistence {
walletConfigDao.addOrUpdate(wallet, wallet.getWalletConfig());
}
if(dirtyPersistables.silentPaymentAddresses) {
SilentPaymentAddressDao silentPaymentAddressDao = handle.attach(SilentPaymentAddressDao.class);
silentPaymentAddressDao.clearAndAddAll(wallet);
}
if(dirtyPersistables.walletTable != null) {
WalletTableDao walletTableDao = handle.attach(WalletTableDao.class);
walletTableDao.addOrUpdate(wallet, dirtyPersistables.walletTable.getTableType(), dirtyPersistables.walletTable);
@ -425,52 +407,6 @@ public class DbPersistence implements Persistence {
}
}
private void validateSchema(Storage storage, String schema, ECKey encryptionKey) throws StorageException {
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
try {
jdbi.useHandle(handle -> {
List<String> routines = handle.createQuery("SELECT ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA <> 'INFORMATION_SCHEMA'").mapTo(String.class).list();
if(!routines.isEmpty()) {
throw new RuntimeException(new StorageException("Wallet file contains unexpected database routines: " + String.join(", ", routines) + "."));
}
List<String> triggers = handle.createQuery("SELECT TRIGGER_NAME FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA <> 'INFORMATION_SCHEMA'").mapTo(String.class).list();
if(!triggers.isEmpty()) {
throw new RuntimeException(new StorageException("Wallet file contains unexpected database triggers: " + String.join(", ", triggers) + "."));
}
List<String> checkConstraints = handle.createQuery("SELECT CHECK_CLAUSE FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS WHERE UPPER(CONSTRAINT_SCHEMA) = UPPER(:schema)")
.bind("schema", schema).mapTo(String.class).list();
if(!checkConstraints.isEmpty()) {
throw new RuntimeException(new StorageException("Wallet file contains unexpected check constraints: " + String.join(", ", checkConstraints) + "."));
}
List<Map<String, Object>> nonBaseTables = handle.createQuery("SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE UPPER(TABLE_SCHEMA) = UPPER(:schema) "
+ "AND TABLE_TYPE <> 'BASE TABLE' AND UPPER(TABLE_NAME) <> 'FLYWAY_SCHEMA_HISTORY'").bind("schema", schema).mapToMap().list();
if(!nonBaseTables.isEmpty()) {
String detail = nonBaseTables.stream().map(m -> m.get("TABLE_NAME") + " (" + m.get("TABLE_TYPE") + ")").collect(Collectors.joining(", "));
throw new RuntimeException(new StorageException("Wallet file contains unexpected database object types: " + detail + "."));
}
List<String> generatedColumns = handle.createQuery("SELECT TABLE_NAME || '.' || COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE UPPER(TABLE_SCHEMA) = UPPER(:schema) AND GENERATION_EXPRESSION IS NOT NULL")
.bind("schema", schema).mapTo(String.class).list();
if(!generatedColumns.isEmpty()) {
throw new RuntimeException(new StorageException("Wallet file contains unexpected generated columns: " + String.join(", ", generatedColumns) + "."));
}
List<String> domains = handle.createQuery("SELECT DOMAIN_NAME FROM INFORMATION_SCHEMA.DOMAINS WHERE DOMAIN_SCHEMA <> 'INFORMATION_SCHEMA'").mapTo(String.class).list();
if(!domains.isEmpty()) {
throw new RuntimeException(new StorageException("Wallet file contains unexpected database domains: " + String.join(", ", domains) + "."));
}
});
} catch(RuntimeException e) {
if(e.getCause() instanceof StorageException) {
throw new StorageException("This is not a valid wallet file.\n\n" + e.getCause().getMessage());
}
throw e;
}
}
private void cleanAndMigrate(Storage storage, String schema, String password) throws StorageException {
File migrationDir = getMigrationDir();
try {
@ -759,10 +695,7 @@ public class DbPersistence implements Persistence {
}
}
private String getUrl(File walletFile, String password) throws StorageException {
if(JDBC_URL_INJECTION_PATTERN.matcher(walletFile.getAbsolutePath()).find()) {
throw new StorageException("Wallet file path contains invalid characters");
}
private String getUrl(File walletFile, String password) {
return "jdbc:h2:" + walletFile.getAbsolutePath().replace("." + getType().getExtension(), "") + ";INIT=SET TRACE_LEVEL_FILE=4;TRACE_LEVEL_FILE=4;DEFRAG_ALWAYS=true;MAX_COMPACT_TIME=5000;DATABASE_TO_UPPER=false" + (password == null ? "" : ";CIPHER=AES");
}
@ -893,13 +826,6 @@ public class DbPersistence implements Persistence {
}
}
@Subscribe
public void walletSilentPaymentAddressesChanged(WalletSilentPaymentAddressesChangedEvent event) {
if(persistsFor(event.getWallet())) {
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).silentPaymentAddresses = true);
}
}
private static class DirtyPersistables {
public boolean deleteAccount;
public boolean clearHistory;
@ -918,7 +844,6 @@ public class DbPersistence implements Persistence {
public final List<Keystore> labelKeystores = new ArrayList<>();
public final List<Keystore> encryptionKeystores = new ArrayList<>();
public final List<Keystore> registrationKeystores = new ArrayList<>();
public boolean silentPaymentAddresses;
public String toString() {
return "Dirty Persistables" +
@ -940,8 +865,7 @@ public class DbPersistence implements Persistence {
"\nUTXO mixes removed:" + removedUtxoMixes +
"\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
"\nKeystore encryptions:" + encryptionKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
"\nKeystore registrations:" + registrationKeystores.stream().map(Keystore::getDeviceRegistration).collect(Collectors.toList()) +
"\nSilent payment addresses:" + silentPaymentAddresses;
"\nKeystore registrations:" + registrationKeystores.stream().map(Keystore::getDeviceRegistration).collect(Collectors.toList());
}
}
}

View File

@ -10,16 +10,16 @@ import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.List;
public interface KeystoreDao {
@SqlQuery("select keystore.id, keystore.label, keystore.source, keystore.walletModel, keystore.masterFingerprint, keystore.derivationPath, keystore.extendedPublicKey, keystore.externalPaymentCode, keystore.silentPaymentScanAddress, keystore.deviceRegistration, " +
@SqlQuery("select keystore.id, keystore.label, keystore.source, keystore.walletModel, keystore.masterFingerprint, keystore.derivationPath, keystore.extendedPublicKey, keystore.externalPaymentCode, keystore.deviceRegistration, " +
"masterPrivateExtendedKey.id, masterPrivateExtendedKey.privateKey, masterPrivateExtendedKey.chainCode, masterPrivateExtendedKey.initialisationVector, masterPrivateExtendedKey.encryptedBytes, masterPrivateExtendedKey.keySalt, masterPrivateExtendedKey.deriver, masterPrivateExtendedKey.crypter, " +
"seed.id, seed.type, seed.mnemonicString, seed.initialisationVector, seed.encryptedBytes, seed.keySalt, seed.deriver, seed.crypter, seed.needsPassphrase, seed.creationTimeSeconds " +
"from keystore left join masterPrivateExtendedKey on keystore.masterPrivateExtendedKey = masterPrivateExtendedKey.id left join seed on keystore.seed = seed.id where keystore.wallet = ? order by keystore.index asc")
@RegisterRowMapper(KeystoreMapper.class)
List<Keystore> getForWalletId(Long id);
@SqlUpdate("insert into keystore (label, source, walletModel, masterFingerprint, derivationPath, extendedPublicKey, externalPaymentCode, silentPaymentScanAddress, deviceRegistration, masterPrivateExtendedKey, seed, wallet, index) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@SqlUpdate("insert into keystore (label, source, walletModel, masterFingerprint, derivationPath, extendedPublicKey, externalPaymentCode, deviceRegistration, masterPrivateExtendedKey, seed, wallet, index) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insert(String label, int source, int walletModel, String masterFingerprint, String derivationPath, String extendedPublicKey, String externalPaymentCode, byte[] silentPaymentScanAddress, byte[] deviceRegistration, Long masterPrivateExtendedKey, Long seed, long wallet, int index);
long insert(String label, int source, int walletModel, String masterFingerprint, String derivationPath, String extendedPublicKey, String externalPaymentCode, byte[] deviceRegistration, Long masterPrivateExtendedKey, Long seed, long wallet, int index);
@SqlUpdate("insert into masterPrivateExtendedKey (privateKey, chainCode, initialisationVector, encryptedBytes, keySalt, deriver, crypter, creationTimeSeconds) values (?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
@ -71,9 +71,8 @@ public interface KeystoreDao {
long id = insert(truncate(keystore.getLabel()), keystore.getSource().ordinal(), keystore.getWalletModel().ordinal(),
keystore.hasMasterPrivateKey() || wallet.isBip47() ? null : keystore.getKeyDerivation().getMasterFingerprint(),
keystore.getKeyDerivation().getDerivationPath(),
keystore.hasMasterPrivateKey() || wallet.isBip47() || keystore.getExtendedPublicKey() == null ? null : keystore.getExtendedPublicKey().toString(),
keystore.hasMasterPrivateKey() || wallet.isBip47() ? null : keystore.getExtendedPublicKey().toString(),
keystore.getExternalPaymentCode() == null ? null : keystore.getExternalPaymentCode().toString(),
keystore.getSilentPaymentScanAddress() == null ? null : keystore.getSilentPaymentScanAddress().toBytes(),
keystore.getDeviceRegistration(),
keystore.getMasterPrivateExtendedKey() == null ? null : keystore.getMasterPrivateExtendedKey().getId(),
keystore.getSeed() == null ? null : keystore.getSeed().getId(), wallet.getId(), i);

View File

@ -5,7 +5,6 @@ import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.drongo.crypto.EncryptedData;
import com.sparrowwallet.drongo.crypto.EncryptionType;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
import com.sparrowwallet.drongo.wallet.*;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
@ -26,7 +25,6 @@ public class KeystoreMapper implements RowMapper<Keystore> {
keystore.setKeyDerivation(new KeyDerivation(rs.getString("keystore.masterFingerprint"), rs.getString("keystore.derivationPath")));
keystore.setExtendedPublicKey(rs.getString("keystore.extendedPublicKey") == null ? null : ExtendedKey.fromDescriptor(rs.getString("keystore.extendedPublicKey")));
keystore.setExternalPaymentCode(rs.getString("keystore.externalPaymentCode") == null ? null : PaymentCode.fromString(rs.getString("keystore.externalPaymentCode")));
keystore.setSilentPaymentScanAddress(rs.getBytes("keystore.silentPaymentScanAddress") == null ? null : SilentPaymentScanAddress.fromBytes(rs.getBytes("keystore.silentPaymentScanAddress")));
keystore.setDeviceRegistration(rs.getBytes("keystore.deviceRegistration"));
if(rs.getBytes("masterPrivateExtendedKey.privateKey") != null) {

View File

@ -1,40 +0,0 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import com.sparrowwallet.drongo.wallet.Wallet;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.statement.SqlBatch;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public interface SilentPaymentAddressDao {
@SqlQuery("select address, silentPaymentAddress from silentPaymentAddress")
@RegisterRowMapper(SilentPaymentAddressMapper.class)
Map<Address, SilentPaymentAddress> getAll();
@SqlBatch("insert into silentPaymentAddress (address, silentPaymentAddress) values (?, ?)")
void insertSilentPaymentAddresses(List<byte[]> addresses, List<byte[]> silentPaymentAddresses);
@SqlUpdate("delete from silentPaymentAddress")
void clear();
default void clearAndAddAll(Wallet wallet) {
clear();
List<byte[]> addresses = new ArrayList<>();
List<byte[]> silentPaymentAddresses = new ArrayList<>();
for(Map.Entry<Address, SilentPaymentAddress> entry : wallet.getSilentPaymentAddresses().entrySet()) {
addresses.add(entry.getKey().getData());
silentPaymentAddresses.add(entry.getValue().serialize());
}
if(!addresses.isEmpty()) {
insertSilentPaymentAddresses(addresses, silentPaymentAddresses);
}
}
}

View File

@ -1,36 +0,0 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.P2TRAddress;
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
public class SilentPaymentAddressMapper implements RowMapper<Map.Entry<Address, SilentPaymentAddress>> {
@Override
public Map.Entry<Address, SilentPaymentAddress> map(ResultSet rs, StatementContext ctx) throws SQLException {
Address address = new P2TRAddress(rs.getBytes("address"));
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.fromBytes(rs.getBytes("silentPaymentAddress"));
return new Map.Entry<>() {
@Override
public Address getKey() {
return address;
}
@Override
public SilentPaymentAddress getValue() {
return silentPaymentAddress;
}
@Override
public SilentPaymentAddress setValue(SilentPaymentAddress value) {
return null;
}
};
}
}

View File

@ -30,9 +30,6 @@ public interface WalletDao {
@CreateSqlObject
DetachedLabelDao createDetachedLabelDao();
@CreateSqlObject
SilentPaymentAddressDao createSilentPaymentAddressDao();
@CreateSqlObject
WalletConfigDao createWalletConfigDao();
@ -45,21 +42,21 @@ public interface WalletDao {
@CreateSqlObject
UtxoMixDataDao createUtxoMixDataDao();
@SqlQuery("select wallet.id, wallet.name, wallet.label, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.watchLast, wallet.birthDate, wallet.birthHeight, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id")
@SqlQuery("select wallet.id, wallet.name, wallet.label, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.watchLast, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id")
@RegisterRowMapper(WalletMapper.class)
List<Wallet> loadAllWallets();
@SqlQuery("select wallet.id, wallet.name, wallet.label, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.watchLast, wallet.birthDate, wallet.birthHeight, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id = 1")
@SqlQuery("select wallet.id, wallet.name, wallet.label, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.watchLast, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id = 1")
@RegisterRowMapper(WalletMapper.class)
Wallet loadMainWallet();
@SqlQuery("select wallet.id, wallet.name, wallet.label, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.watchLast, wallet.birthDate, wallet.birthHeight, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id != 1")
@SqlQuery("select wallet.id, wallet.name, wallet.label, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.watchLast, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id != 1")
@RegisterRowMapper(WalletMapper.class)
List<Wallet> loadChildWallets();
@SqlUpdate("insert into wallet (name, label, network, policyType, scriptType, storedBlockHeight, gapLimit, watchLast, birthDate, birthHeight, defaultPolicy) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@SqlUpdate("insert into wallet (name, label, network, policyType, scriptType, storedBlockHeight, gapLimit, watchLast, birthDate, defaultPolicy) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insert(String name, String label, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Integer watchLast, Date birthDate, Integer birthHeight, long defaultPolicy);
long insert(String name, String label, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Integer watchLast, Date birthDate, long defaultPolicy);
@SqlUpdate("update wallet set name = :name where id = :id")
void updateName(@Bind("id") long id, @Bind("name") String name);
@ -115,7 +112,7 @@ public interface WalletDao {
wallet.getKeystores().addAll(createKeystoreDao().getForWalletId(wallet.getId()));
List<WalletNode> walletNodes = createWalletNodeDao().getForWalletId(wallet.getScriptType().ordinal(), wallet.getId());
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(WalletNode::isPurposeNode).collect(Collectors.toList()));
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList()));
wallet.getPurposeNodes().forEach(walletNode -> walletNode.setWallet(wallet));
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId());
@ -124,8 +121,6 @@ public interface WalletDao {
Map<String, String> detachedLabels = createDetachedLabelDao().getAll();
wallet.getDetachedLabels().putAll(detachedLabels);
wallet.getSilentPaymentAddresses().putAll(createSilentPaymentAddressDao().getAll());
wallet.setWalletConfig(createWalletConfigDao().getForWalletId(wallet.getId()));
Map<TableType, WalletTable> walletTables = createWalletTableDao().getForWalletId(wallet.getId());
@ -142,14 +137,13 @@ public interface WalletDao {
setSchema(schema);
createPolicyDao().addPolicy(wallet.getDefaultPolicy());
long id = insert(truncate(wallet.getName()), truncate(wallet.getLabel()), wallet.getNetwork().ordinal(), wallet.getPolicyType().ordinal(), wallet.getScriptType().ordinal(), wallet.getStoredBlockHeight(), wallet.gapLimit(), wallet.getWatchLast(), wallet.getBirthDate(), wallet.getBirthHeight(), wallet.getDefaultPolicy().getId());
long id = insert(truncate(wallet.getName()), truncate(wallet.getLabel()), wallet.getNetwork().ordinal(), wallet.getPolicyType().ordinal(), wallet.getScriptType().ordinal(), wallet.getStoredBlockHeight(), wallet.gapLimit(), wallet.getWatchLast(), wallet.getBirthDate(), wallet.getDefaultPolicy().getId());
wallet.setId(id);
createKeystoreDao().addKeystores(wallet);
createWalletNodeDao().addWalletNodes(wallet);
createBlockTransactionDao().addBlockTransactions(wallet);
createDetachedLabelDao().clearAndAddAll(wallet);
createSilentPaymentAddressDao().clearAndAddAll(wallet);
createWalletConfigDao().addWalletConfig(wallet);
createWalletTableDao().addWalletTables(wallet);
createMixConfigDao().addMixConfig(wallet);

View File

@ -34,8 +34,6 @@ public class WalletMapper implements RowMapper<Wallet> {
int watchLast = rs.getInt("wallet.watchLast");
wallet.setWatchLast(rs.wasNull() ? null : watchLast);
wallet.setBirthDate(rs.getTimestamp("wallet.birthDate"));
int birthHeight = rs.getInt("wallet.birthHeight");
wallet.setBirthHeight(rs.wasNull() ? null : birthHeight);
return wallet;
}

View File

@ -16,7 +16,7 @@ import java.util.Date;
import java.util.List;
public interface WalletNodeDao {
@SqlQuery("select walletNode.id, walletNode.derivationPath, walletNode.label, walletNode.parent, walletNode.addressData, walletNode.silentPaymentTweak, ?, " +
@SqlQuery("select walletNode.id, walletNode.derivationPath, walletNode.label, walletNode.parent, walletNode.addressData, ?, " +
"blockTransactionHashIndex.id, blockTransactionHashIndex.hash, blockTransactionHashIndex.height, blockTransactionHashIndex.date, blockTransactionHashIndex.fee, blockTransactionHashIndex.label, " +
"blockTransactionHashIndex.index, blockTransactionHashIndex.outputValue, blockTransactionHashIndex.status, blockTransactionHashIndex.spentBy, blockTransactionHashIndex.node " +
"from walletNode left join blockTransactionHashIndex on walletNode.id = blockTransactionHashIndex.node where walletNode.wallet = ? order by walletNode.parent asc nulls first, blockTransactionHashIndex.spentBy asc nulls first")
@ -25,9 +25,9 @@ public interface WalletNodeDao {
@UseRowReducer(WalletNodeReducer.class)
List<WalletNode> getForWalletId(int scriptType, Long id);
@SqlUpdate("insert into walletNode (derivationPath, label, wallet, parent, addressData, silentPaymentTweak) values (?, ?, ?, ?, ?, ?)")
@SqlUpdate("insert into walletNode (derivationPath, label, wallet, parent, addressData) values (?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insertWalletNode(String derivationPath, String label, long wallet, Long parent, byte[] addressData, byte[] silentPaymentTweak);
long insertWalletNode(String derivationPath, String label, long wallet, Long parent, byte[] addressData);
@SqlUpdate("insert into blockTransactionHashIndex (hash, height, date, fee, label, index, outputValue, status, spentBy, node) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
@ -62,12 +62,12 @@ public interface WalletNodeDao {
default void addWalletNodes(Wallet wallet) {
for(WalletNode purposeNode : wallet.getPurposeNodes()) {
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), truncate(purposeNode.getLabel()), wallet.getId(), null, null, null);
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), truncate(purposeNode.getLabel()), wallet.getId(), null, null);
purposeNode.setId(purposeNodeId);
addTransactionOutputs(purposeNode);
List<WalletNode> childNodes = new ArrayList<>(purposeNode.getChildren());
for(WalletNode addressNode : childNodes) {
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), truncate(addressNode.getLabel()), wallet.getId(), purposeNodeId, addressNode.getAddressData(), addressNode.getSilentPaymentTweak());
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), truncate(addressNode.getLabel()), wallet.getId(), purposeNodeId, addressNode.getAddressData());
addressNode.setId(addressNodeId);
addTransactionOutputs(addressNode);
}

Some files were not shown because too many files have changed in this diff Show More