Compare commits

...

81 Commits

Author SHA1 Message Date
Craig Raw
3dc50682a3 upgrade lanterna to v3.1.5 2026-06-23 09:32:44 +02:00
Craig Raw
d5b119e338 bump to v2.5.3 2026-05-31 14:24:55 +02:00
Craig Raw
b457caa5d2 revise wording for non-default sighash warnings 2026-05-31 12:27:16 +02:00
Craig Raw
61ed816c87 improve verification of psbt sighash types 2026-05-31 11:55:38 +02:00
Craig Raw
9a603d7547 make date axis formatter tests locale-stable 2026-05-30 17:57:05 +02:00
PeterXMR
ac666545be
show full year on balance chart x-axis 2026-05-30 16:54:27 +02:00
Craig Raw
c4c77d84e0 use configured unit format in send to many dialog instead of jvm default 2026-05-30 14:04:09 +02:00
Craig Raw
8d126869a6 fix potential off by 1 sat rounding error on imported send to many dialog amounts 2026-05-30 12:32:05 +02:00
Craig Raw
464fade68f improve url validation for auth47 and lnurl-auth 2026-05-30 11:34:53 +02:00
Craig Raw
79517da131 fix potential npes resulting from get transactions 2026-05-28 14:51:24 +02:00
Craig Raw
086297436e followup for precomputed sp outputs 2026-05-26 15:40:31 +02:00
Craig Raw
d69e274b18 warn on loading transactions with non-zero outputs of unknown script type 2026-05-26 12:37:27 +02:00
Craig Raw
d1e67ad4a0 update lark for hid4java 2026-05-24 19:01:46 +02:00
Craig Raw
37ca98c2b0 implement dust detection for sp wallets on received utxos at a higher default limit 2026-05-24 11:23:33 +02:00
Craig Raw
287c943b44 add custom context menu to signature text area in message sign dialog 2026-05-23 15:17:08 +02:00
Craig Raw
cb92f76546 improve loaded psbt verification 2026-05-23 14:52:35 +02:00
Craig Raw
24e9c39cb8 bump to v2.5.2 2026-05-22 18:35:16 +02:00
Craig Raw
754ebf7bbf fix incorrect script type selected in settings on p2tr wallet load 2026-05-22 17:32:41 +02:00
Craig Raw
bc7a0be87e update bip322 implementation to match completed spec 2026-05-22 15:00:53 +02:00
Craig Raw
87af1ed9f5 fix potential npe on transaction entry tooltip 2026-05-22 10:28:17 +02:00
Craig Raw
da476c9d77 bump to v2.5.1 2026-05-21 13:38:15 +02:00
Craig Raw
d8e4582b3c minor ui tweaks followup 2026-05-21 11:45:57 +02:00
Craig Raw
ad5c695946 minor ui tweaks 2026-05-21 11:20:11 +02:00
Craig Raw
3150a96aab bump to v2.5.0 2026-05-21 09:27:26 +02:00
Craig Raw
28521cbc1a update wallet settings help labels 2026-05-20 19:49:49 +02:00
Craig Raw
79bbe7df9f finalize external inputs in cross-wallet psbts to avoid empty witnesses 2026-05-19 18:16:08 +02:00
Craig Raw
e339c9f51a extend post-broadcast mempool poll timeout to support bitcoin core privatebroadcast 2026-05-19 12:27:41 +02:00
Craig Raw
6dd1bda0cb update jzbar to v0.4.0 2026-05-19 11:28:00 +02:00
Craig Raw
9fe0d17aac add frigate.2140.dev public electrum server and auto-select based on requirements for open wallets 2026-05-19 11:04:04 +02:00
Craig Raw
a2eb937fce add bip322 message signing for silent payments wallets 2026-05-18 14:56:50 +02:00
Craig Raw
e985a03a58 persist silent payment address mappings for safe rbf of sp-sending transactions 2026-05-18 12:41:56 +02:00
Craig Raw
273d2aacf3 release electrum transport read lock during socket reads to avoid client request starvation 2026-05-15 11:00:06 +02:00
Craig Raw
97383a4e35 restrict to required sighash types when sending sp outputs 2026-05-15 09:35:29 +02:00
Craig Raw
712750a25c hide receive actions for address entry cells in sp wallets 2026-05-15 08:15:44 +02:00
Craig Raw
1be9ac1072 hide wallet rescan hyperlink when nothing further can be scanned 2026-05-14 12:40:02 +02:00
Craig Raw
a035767e38 default sp wallet birthdate to creation time to avoid full rescans 2026-05-14 11:23:12 +02:00
Craig Raw
1ad237c623 switch electrum server notification detection to streaming json token parse 2026-05-14 10:10:41 +02:00
Craig Raw
7e4aaacd2f preserve sp scan cache cancellation across widening rollback 2026-05-13 13:58:50 +02:00
Craig Raw
60d4cce15b pre-populate sp self-spend node outputs to avoid partial first notification 2026-05-13 11:29:30 +02:00
Craig Raw
7afb27b3e5 increase read timeouts when tor is configured 2026-05-12 16:29:00 +02:00
Craig Raw
8f56e95048 remove bisq broadcast source 2026-05-12 16:19:18 +02:00
Craig Raw
1a5f97cecf discard stale electrum responses with mismatched ids 2026-05-12 15:32:05 +02:00
Craig Raw
db3ebdb300 update bisq broadcast source urls 2026-05-12 15:18:35 +02:00
Craig Raw
0b012995a0 improve wallet update behaviour for silent payment self-sends 2026-05-12 15:17:51 +02:00
Craig Raw
9b12361724 prevent tweakless address nodes in sp wallets 2026-05-08 09:27:08 +02:00
Craig Raw
201d4b8376 refactor transaction diagram to dispatch on output wrapper types 2026-05-07 09:38:15 +02:00
Craig Raw
ab6416f30a fix sp self-spend output discovery by always scanning all batch txs 2026-05-06 15:05:52 +02:00
Craig Raw
e64069f02f implement sp wallet loading 2026-05-04 14:02:52 +02:00
Craig Raw
e96a7113b0 refactor transaction history fetch method 2026-04-30 12:28:46 +02:00
Craig Raw
abe1f9c9e5 add silent payments rpc methods, capability check and notification dispatch 2026-04-29 13:28:44 +02:00
Craig Raw
69aef9c228 add all singlesig importers to sp airgapped keystore import 2026-04-28 09:35:56 +02:00
Craig Raw
630ce0f3ed update bitview logo 2026-04-27 14:59:04 +02:00
Craig Raw
723b004e5f support sp wallet import via all keystore importers 2026-04-27 12:40:33 +02:00
Craig Raw
6cefec5cec avoid unnecessary xpub string roundtrip 2026-04-27 08:28:09 +02:00
Craig Raw
f5ee7bf277 support retrieving silent payments spscan keys via connected devices 2026-04-27 08:13:35 +02:00
Craig Raw
0459f4ca45 add policy type to all keystore import interfaces and factory methods for explicit keystore xpub or spscan field population 2026-04-26 10:17:09 +02:00
Craig Raw
c23dbeedcf improve sp related output descriptor and psbt behaviour 2026-04-24 12:37:07 +02:00
Craig Raw
0bb5e9319f hide gap limit field for sp wallets 2026-04-23 13:53:44 +02:00
Craig Raw
1c5602aa9d implement silent payments receive ui 2026-04-23 13:46:40 +02:00
Craig Raw
c67008840d implement silent payments change outputs and other sp related fixes 2026-04-23 12:23:40 +02:00
Craig Raw
17a04510fd add initial support for sp wallets 2026-04-22 14:57:30 +02:00
Craig Raw
dd50f6973b implement persistence for sp wallets and related drongo changes 2026-04-21 11:49:16 +02:00
Craig Raw
8780e515d5 initial policy type related changes from drongo 2026-04-21 11:34:14 +02:00
Craig Raw
110f887952 add bitview.space as fee rates source 2026-04-09 11:32:20 +02:00
Craig Raw
1143eaa55f only allow sending to payment codes where a notification transaction has previously been sent, even when already linked 2026-04-08 13:51:54 +02:00
Craig Raw
3068ba3988 improve appearance of app notifications after controlsfx upgrade 2026-04-08 12:38:01 +02:00
Craig Raw
58ca52e4c7 upgrade javafx to v26 with headless plaform 2026-04-07 12:36:49 +02:00
Craig Raw
883b7cf3b2 fix concurrent modification of descriptor maps in bitcoind client 2026-04-06 09:12:13 +02:00
Craig Raw
ee5b502a00 improve handling of connected non-jade esp32 devices 2026-03-26 12:14:22 +02:00
Craig Raw
4d7baa070e fix regression to restore save pdf button on descriptor qr display dialog 2026-03-17 11:11:20 +02:00
Craig Raw
fb48643466 load jzbar native dependencies explicitly 2026-03-16 12:00:10 +02:00
Craig Raw
e2c38c2ac4 upgrade jzbar to v0.3.1 to fix library load from application image 2026-03-16 11:33:00 +02:00
Craig Raw
e55dd4401b upgrade usb4java to v1.3.6 to fix library load from application image 2026-03-13 10:42:56 +02:00
Craig Raw
53b6e2532f temporarily revert jzbar application image loading 2026-03-13 09:09:13 +02:00
Craig Raw
98ae891898 load native libraries directly from application image 2026-03-13 07:59:39 +02:00
Craig Raw
019b11c95e upgrade usb4java to allow loading from libraryname system property 2026-03-12 15:38:44 +02:00
Craig Raw
186eacc245 validate bip129 header on import, and fix importing unencrypted bsms files with open wallet 2026-03-12 12:33:56 +02:00
Craig Raw
32a35ed2c0 add bip32 derivation fallback when retreiving signing nodes for high-index inputs 2026-03-11 17:31:26 +02:00
Craig Raw
21f9f9fe25 avoid npe when the extracting signature from a bip322 psbt 2026-03-10 12:07:59 +02:00
Craig Raw
6c6664f29a use psbtv0 for bip322 psbt qr and file exports 2026-03-10 11:35:28 +02:00
Craig Raw
ea0509ca3d bump to v2.4.3 2026-03-10 11:26:47 +02:00
172 changed files with 4065 additions and 1646 deletions

View File

@ -1,6 +1,6 @@
plugins {
id 'application'
id 'org-openjfx-javafxplugin'
id 'org.openjfx.javafxplugin' version '0.1.0'
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.4.2'
version = '2.5.3'
repositories {
mavenCentral()
@ -32,7 +32,7 @@ tasks.withType(AbstractArchiveTask).configureEach {
}
javafx {
version = headless ? "18" : "25.0.2"
version = "26"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
@ -104,12 +104,12 @@ dependencies {
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.3')
implementation('com.googlecode.lanterna:lanterna:3.1.5')
implementation('net.coobird:thumbnailator:0.4.21')
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.3.0')
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')
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
@ -124,14 +124,6 @@ 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"]
@ -170,7 +162,7 @@ application {
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"]
}
if(headless) {
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
applicationDefaultJvmArgs += ["-Dglass.platform=Headless"]
}
}
@ -188,7 +180,34 @@ 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/*']
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/**']
launcher {
name = 'sparrow'
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
@ -198,6 +217,7 @@ 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",
@ -236,7 +256,7 @@ jlink {
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
}
if(headless) {
jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
jvmArgs += ["-Dglass.platform=Headless"]
}
}
addExtraDependencies("javafx")
@ -277,13 +297,13 @@ jlink {
}
if(os.linux) {
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules', 'extractNativeLibraries')
tasks.jpackageImage.finalizedBy('prepareResourceDir')
if(!headless) {
tasks.jpackage.dependsOn('copyMimeInfo')
}
} else {
tasks.jlink.finalizedBy('addUserWritePermission')
tasks.jlink.finalizedBy('addUserWritePermission', 'extractNativeLibraries')
}
tasks.register('addUserWritePermission', Exec) {
@ -362,6 +382,74 @@ 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')

View File

@ -1,23 +0,0 @@
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

@ -1,114 +0,0 @@
/*
* 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

@ -1,164 +0,0 @@
/*
* 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

@ -1,91 +0,0 @@
/*
* 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

@ -1,47 +0,0 @@
/*
* 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

@ -1,90 +0,0 @@
/*
* 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

@ -56,7 +56,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.4.1"
GIT_TAG="2.5.2"
```
The project can then be initially cloned as follows:

2
drongo

@ -1 +1 @@
Subproject commit 4049ebcdda27140473ddd618e9f7cd0395706ba0
Subproject commit 077d2142cc3aad84f6f58868cf8f17fc61027fdc

2
lark

@ -1 +1 @@
Subproject commit 7f79ddee6baaf89e618acbaa1425df6636c8bbca
Subproject commit e9c6f35fe66aee105ef3c532fcefeb7130dab169

View File

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

View File

@ -88,7 +88,6 @@ 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;
@ -420,7 +419,7 @@ public class AppController implements Initializable {
networkItem.setOnAction(event -> restart(event, network));
restart.getItems().add(networkItem);
}
restart.setVisible(System.getProperty(JPACKAGE_APP_PATH) != null);
restart.setVisible(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH) != null);
saveTransaction.setDisable(true);
showTransaction.visibleProperty().bind(Bindings.and(saveTransaction.visibleProperty(), saveTransaction.disableProperty().not()));
@ -472,7 +471,7 @@ public class AppController implements Initializable {
private void setPlatformApplicationMenu() {
OsType osType = OsType.getCurrent();
if(osType == OsType.MACOS) {
if(osType == OsType.MACOS && Interface.get() == Interface.DESKTOP) {
MenuToolkit tk = MenuToolkit.toolkit();
MenuItem settings = new MenuItem("Settings...");
settings.setOnAction(this::openSettings);
@ -597,7 +596,7 @@ public class AppController implements Initializable {
sudo groupadd -f -r plugdev
sudo usermod -aG plugdev `whoami`
""";
String home = System.getProperty(JPACKAGE_APP_PATH);
String home = System.getProperty(SparrowWallet.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);
@ -1045,8 +1044,8 @@ public class AppController implements Initializable {
}
public void restart(ActionEvent event, Network network) {
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
if(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH) == null) {
throw new IllegalStateException("Property " + SparrowWallet.JPACKAGE_APP_PATH + " is not present");
}
Args args = getRestartArgs();
@ -1067,7 +1066,7 @@ public class AppController implements Initializable {
private void restart(ActionEvent event, Args args) {
try {
List<String> cmd = new ArrayList<>();
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
cmd.add(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH));
cmd.addAll(args.toParams());
final ProcessBuilder builder = new ProcessBuilder(cmd);
if(OsType.getCurrent() == OsType.UNIX) {
@ -1122,7 +1121,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, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate());
Wallet wallet = new Wallet(nameAndBirthDate.getName(), PolicyType.SINGLE_HD, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate());
addWalletTabOrWindow(storage, wallet, false);
}
}
@ -1272,7 +1271,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 &&
.filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI_HD &&
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()) {
@ -1287,6 +1286,7 @@ 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,13 +1328,16 @@ public class AppController implements Initializable {
return;
}
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getPolicyType(), wallet.getBirthDate(), false);
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;
}
@ -1473,7 +1476,7 @@ public class AppController implements Initializable {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
Wallet wallet = selectedWalletForm.getWallet();
if(wallet.getKeystores().size() == 1) {
if(wallet.getPolicyType() == PolicyType.SINGLE_HD || wallet.getPolicyType() == PolicyType.SINGLE_SP) {
//Can sign and verify
messageSignDialog = new MessageSignDialog(wallet);
}
@ -1508,7 +1511,7 @@ public class AppController implements Initializable {
bitcoinUnit = wallet.getAutoUnit();
}
sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
sendToManyDialog = new SendToManyDialog(bitcoinUnit, Config.get().getUnitFormat(), initialPayments);
sendToManyDialog.initModality(Modality.NONE);
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
sendToManyDialog = null;
@ -2055,6 +2058,34 @@ 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 {
@ -2161,6 +2192,23 @@ 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,9 +69,11 @@ 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 {
@ -367,15 +369,18 @@ public class AppServices {
onlineProperty.setValue(false);
onlineProperty.addListener(onlineServicesListener);
log.debug("Connection failed", failEvent.getSource().getException());
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
Config.get().changePublicServer();
connectionService.setPeriod(Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS));
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...")));
}
} 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;
@ -851,6 +856,10 @@ 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);
@ -862,6 +871,22 @@ 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);
}
@ -1077,7 +1102,7 @@ public class AppServices {
try {
Auth47 auth47 = new Auth47(uri);
List<ScriptType> scriptTypes = PaymentCode.SEGWIT_SCRIPT_TYPES;
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE_HD), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
if(wallet != null) {
try {
@ -1097,8 +1122,8 @@ public class AppServices {
private static void openLnurlAuthUri(URI uri) {
try {
LnurlAuth lnurlAuth = new LnurlAuth(uri);
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD);
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE_HD), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
if(wallet != null) {
if(wallet.isEncrypted()) {
@ -1212,6 +1237,7 @@ 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()
@ -1222,6 +1248,7 @@ 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
@ -1457,10 +1484,28 @@ 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);
log.warn("Failed to fetch wallet history from " + Config.get().getServerDisplayName() + ", reconnecting to another server...");
Config.get().changePublicServer();
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..."));
}
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 ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_HD || descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_SP ? 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,7 +75,11 @@ public abstract class BaseController {
builder.append(keystore.getKeyDerivation().getMasterFingerprint());
builder.append(KeyDerivation.writePath(KeyDerivation.parsePath(keystore.getKeyDerivation().getDerivationPath())).substring(1));
builder.append("]");
builder.append(keystore.getExtendedPublicKey().toString());
if(keystore.getExtendedPublicKey() != null) {
builder.append(keystore.getExtendedPublicKey().toString());
} else if(keystore.getSilentPaymentScanAddress() != null) {
builder.append(keystore.getSilentPaymentScanAddress().toKeyString());
}
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 monocle = "Monocle".equalsIgnoreCase(System.getProperty("glass.platform"));
boolean headlessPlatform = "Headless".equalsIgnoreCase(System.getProperty("glass.platform"));
if(headless || monocle) {
if(headless || headlessPlatform) {
currentInterface = TERMINAL;
if(headless && !monocle) {
throw new UnsupportedOperationException("Headless environment detected but Monocle platform not found");
if(headless && !headlessPlatform) {
throw new UnsupportedOperationException("Headless environment detected but headless glass platform not found");
}
} else {
currentInterface = DESKTOP;

View File

@ -18,14 +18,24 @@ 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.4.2";
public static final String APP_VERSION = "2.5.3";
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,5 +1,6 @@
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;
@ -68,7 +69,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.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|| (masterWallet.getPolicyType() == PolicyType.SINGLE_HD && 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

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

View File

@ -3,6 +3,7 @@ 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.*;
@ -44,6 +45,7 @@ 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("");
@ -51,6 +53,7 @@ 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();
}
@ -59,7 +62,7 @@ public class CardImportPane extends TitledDescriptionPane {
return defaultDerivation.getDerivation();
}
return wallet == null || wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
return wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
}
@Override
@ -111,7 +114,7 @@ public class CardImportPane extends TitledDescriptionPane {
return;
}
CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty);
CardImportService cardImportService = new CardImportService(importer, policyType, pin.get(), derivation, messageProperty);
cardImportService.setOnSucceeded(event -> {
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
});
@ -352,12 +355,14 @@ 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, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
public CardImportService(KeystoreCardImport cardImport, PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
this.cardImport = cardImport;
this.policyType = policyType;
this.pin = pin;
this.derivation = derivation;
this.messageProperty = messageProperty;
@ -368,7 +373,7 @@ public class CardImportPane extends TitledDescriptionPane {
return new Task<>() {
@Override
protected Keystore call() throws Exception {
return cardImport.getKeystore(pin, derivation, messageProperty);
return cardImport.getKeystore(policyType, 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(derivation, secretShareProperty.get());
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), 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(ScriptType.P2WPKH.getDefaultDerivation(), secretShareProperty.get());
importer.getKeystore(wallet.getPolicyType(), ScriptType.P2WPKH.getDefaultDerivation(), secretShareProperty.get());
validChecksum = true;
} catch(ImportException e) {
invalidLabel.setText("Invalid checksum");

View File

@ -1,6 +1,7 @@
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;
@ -15,7 +16,10 @@ 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;
@ -106,7 +110,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() + "..."));
setPlaceholder(new Label(event.getStatusMessage() + (event.getStatusMessage().contains("...") ? "" : "...")));
} else {
setPlaceholder(new Label("Loading transactions..."));
}
@ -122,7 +126,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 && !AppServices.isConnecting()) {
if((Config.get().getServerType() == ServerType.BITCOIN_CORE || wallet.getPolicyType() == PolicyType.SINGLE_SP) && !AppServices.isConnecting() && !isFullyScanned(wallet)) {
Hyperlink hyperlink = new Hyperlink();
hyperlink.setTranslateY(30);
hyperlink.setOnAction(event -> {
@ -133,6 +137,7 @@ 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
@ -148,12 +153,47 @@ 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) {
@ -208,7 +248,6 @@ public class CoinTreeTable extends TreeTableView<Entry> {
return null;
}
@SuppressWarnings("deprecation")
protected void setupColumnWidths() {
Double[] savedWidths = getSavedColumnWidths();
for(int i = 0; i < getColumns().size(); i++) {
@ -216,8 +255,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
column.setPrefWidth(savedWidths != null && getColumns().size() == savedWidths.length ? savedWidths[i] : STANDARD_WIDTH);
}
//TODO: Replace with TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN when JavaFX 20+ has headless support
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
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 yy");
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yyyy");
private final DateFormat dateFormat;
private int oddCounter;

View File

@ -14,8 +14,7 @@ import org.fxmisc.richtext.CodeArea;
import java.util.List;
import static com.sparrowwallet.drongo.policy.PolicyType.MULTI;
import static com.sparrowwallet.drongo.policy.PolicyType.SINGLE;
import static com.sparrowwallet.drongo.policy.PolicyType.*;
import static com.sparrowwallet.drongo.protocol.ScriptType.MULTISIG;
public class DescriptorArea extends CodeArea {
@ -33,13 +32,13 @@ public class DescriptorArea extends CodeArea {
List<Keystore> keystores = wallet.getKeystores();
int threshold = wallet.getDefaultPolicy().getNumSignaturesRequired();
if(SINGLE.equals(policyType)) {
if(SINGLE_HD.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.equals(policyType)) {
if(MULTI_HD.equals(policyType)) {
append(scriptType.getDescriptor(), "descriptor-text");
append(MULTISIG.getDescriptor(), "descriptor-text");
append(Integer.toString(threshold), "descriptor-text");
@ -52,6 +51,12 @@ 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,5 +23,10 @@ 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,6 +12,7 @@ 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;
@ -304,15 +305,34 @@ public class DevicePane extends TitledDescriptionPane {
if(importButton instanceof SplitMenuButton importMenuButton) {
if(wallet.getScriptType() == null) {
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);
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);
}
}
importMenuButton.getItems().add(new SeparatorMenuItem());
MenuItem discoverItem = new MenuItem("Discover Wallet...");
@ -711,7 +731,7 @@ public class DevicePane extends TitledDescriptionPane {
return;
}
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
Service<Keystore> importService = cardApi.getImportService(wallet.getPolicyType(), derivation, messageProperty);
handleCardOperation(importService, importButton, "Import", true, event -> {
importKeystore(derivation, importService.getValue());
});
@ -730,13 +750,21 @@ public class DevicePane extends TitledDescriptionPane {
}
}
importXpub(derivation);
importKey(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);
}
@ -747,7 +775,7 @@ public class DevicePane extends TitledDescriptionPane {
Hwi.GetXpubService getXpubService = new Hwi.GetXpubService(device, passphrase.get(), derivationPath);
getXpubService.setOnSucceeded(workerStateEvent -> {
String xpub = getXpubService.getValue();
ExtendedKey xpub = getXpubService.getValue();
try {
Keystore keystore = new Keystore();
@ -755,7 +783,7 @@ public class DevicePane extends TitledDescriptionPane {
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
keystore.setExtendedPublicKey(xpub);
importKeystore(derivation, keystore);
} catch(Exception e) {
@ -771,14 +799,44 @@ 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().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
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;
wallet.setName(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setPolicyType(policyType);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
@ -926,7 +984,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));
List<ScriptType> scriptTypes = new ArrayList<>(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD));
if(device.getModel() == WalletModel.BITBOX_02) {
scriptTypes.remove(ScriptType.P2PKH);
}
@ -938,21 +996,21 @@ public class DevicePane extends TitledDescriptionPane {
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), derivationPaths);
getXpubsService.setOnSucceeded(_ -> {
Map<Hwi.WalletType, String> accountXpubs = getXpubsService.getValue();
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
for(Map.Entry<Hwi.WalletType, String> entry : accountXpubs.entrySet()) {
for(Map.Entry<Hwi.WalletType, ExtendedKey> entry : accountXpubs.entrySet()) {
try {
Wallet wallet = new Wallet(device.getModel().toDisplayString());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setPolicyType(PolicyType.SINGLE_HD);
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(ExtendedKey.fromDescriptor(entry.getValue()));
keystore.setExtendedPublicKey(entry.getValue());
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, entry.getKey().scriptType(), wallet.getKeystores(), 1));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, entry.getKey().scriptType(), wallet.getKeystores(), 1));
if(entry.getKey().standardAccount().equals(StandardAccount.ACCOUNT_0)) {
wallets.add(wallet);
} else {
@ -982,7 +1040,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 a wallet with existing transactions using the " + device.getModel().toDisplayString() + ".");
"Could not find an HD wallet with existing transactions using the " + device.getModel().toDisplayString() + ".");
setDefaultStatus();
importButton.setDisable(false);
}
@ -1032,16 +1090,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, String> accountXpubs = getXpubsService.getValue();
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
for(Map.Entry<Hwi.WalletType, String> entry : accountXpubs.entrySet()) {
for(Map.Entry<Hwi.WalletType, ExtendedKey> 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(ExtendedKey.fromDescriptor(entry.getValue()));
keystore.setExtendedPublicKey(entry.getValue());
importedKeystores.put(entry.getKey().standardAccount(), keystore);
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
@ -1179,7 +1237,7 @@ public class DevicePane extends TitledDescriptionPane {
showHideLink.setVisible(true);
setExpanded(false);
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
importXpub(importDerivation);
importKey(importDerivation);
});
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
@ -1355,4 +1413,10 @@ 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,6 +24,7 @@ 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

@ -4,6 +4,7 @@ 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;
@ -135,7 +136,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
HBox actionBox = new HBox();
actionBox.getStyleClass().add("cell-actions");
if(!nodeEntry.getNode().getWallet().isBip47()) {
if(!nodeEntry.getNode().getWallet().isBip47() && nodeEntry.getNode().getWallet().getPolicyType() != PolicyType.SINGLE_SP) {
Button receiveButton = new Button("");
receiveButton.setGraphic(getReceiveGlyph());
receiveButton.setOnAction(event -> {
@ -249,7 +250,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().getFreshNode(KeyPurpose.CHANGE).getOutputScript());
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().getNode(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);
@ -334,8 +335,10 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
if(cancelTransaction) {
Payment existing = payments.get(0);
Address address = transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress();
Payment payment = new Payment(address, existing.getLabel(), existing.getAmount(), true);
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);
payments.clear();
payments.add(payment);
opReturns.clear();
@ -369,10 +372,10 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
}
BlockTransactionHashIndex cpfpUtxo = ourOutputs.get(0);
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();
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();
double vSize = inputSize + txOutput.getLength();
List<TxoFilter> txoFilters = List.of(new ExcludeTxoFilter(List.of(cpfpUtxo)), new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(transactionEntry.getWallet()));
@ -396,7 +399,10 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
Payment payment = new Payment(freshAddress, label, inputTotal, true);
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);
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)));
@ -408,7 +414,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static boolean canSignMessage(WalletNode walletNode) {
Wallet wallet = walletNode.getWallet();
return wallet.getKeystores().size() == 1 && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
PolicyType policyType = wallet.getPolicyType();
return (policyType == PolicyType.SINGLE_HD || policyType == PolicyType.SINGLE_SP) && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
}
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
@ -663,7 +670,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()) {
if(nodeEntry == null || (!nodeEntry.getWallet().isBip47() && nodeEntry.getWallet().getPolicyType() != PolicyType.SINGLE_SP)) {
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.getScriptType(), inputStream, password);
keystore = importer.getKeystore(wallet.getPolicyType(), 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.CryptoOutput;
import com.sparrowwallet.hummingbird.registry.RegistryItem;
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.getCryptoOutput;
import static com.sparrowwallet.sparrow.wallet.SettingsController.getUROutputDescriptor;
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);
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
RegistryItem registryItem = getUROutputDescriptor(exportWallet);
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, encoding);
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), registryItem.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,19 +50,27 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
this.fileName = fileName;
this.password = password;
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
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));
}
}
if(wallets != null && !wallets.isEmpty()) {
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));
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));
wallet.setName(importer.getName());
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");
}
EventManager.get().post(new WalletImportEvent(wallet));
return;
}
} else {
try {
@ -72,58 +80,61 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
}
}
setContent(getScriptTypeEntry(scriptTypes));
setContent(getScriptTypeEntry(types));
setExpanded(true);
importButton.setDisable(true);
}
private void importWallet(ScriptType scriptType) throws ImportException {
private void importWallet(PolicyAndScriptType type) throws ImportException {
PolicyType policyType = type.policyType();
ScriptType scriptType = type.scriptType();
if(wallets != null && !wallets.isEmpty()) {
Wallet wallet = wallets.stream().filter(wallet1 -> wallet1.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == policyType && w.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
wallet.setName(importer.getName());
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
} else {
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
Keystore keystore = importer.getKeystore(scriptType, bais, password);
Keystore keystore = importer.getKeystore(policyType, scriptType, bais, password);
Wallet wallet = new Wallet();
wallet.setName(Files.getNameWithoutExtension(fileName));
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setPolicyType(policyType);
wallet.setScriptType(scriptType);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
EventManager.get().post(new WalletImportEvent(wallet));
}
}
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
Label label = new Label("Script Type:");
private Node getScriptTypeEntry(List<PolicyAndScriptType> types) {
Label label = new Label("Type:");
HBox fieldBox = new HBox(5);
fieldBox.setAlignment(Pos.CENTER_RIGHT);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
if(scriptTypes.contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
ComboBox<PolicyAndScriptType> comboBox = new ComboBox<>(FXCollections.observableArrayList(types));
PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH);
if(types.contains(defaultType)) {
comboBox.setValue(defaultType);
}
scriptTypeComboBox.setConverter(new StringConverter<>() {
comboBox.setConverter(new StringConverter<>() {
@Override
public String toString(ScriptType scriptType) {
return scriptType == null ? "" : scriptType.getDescription();
public String toString(PolicyAndScriptType type) {
return type == null ? "" : type.getDescription();
}
@Override
public ScriptType fromString(String string) {
public PolicyAndScriptType fromString(String string) {
return null;
}
});
scriptTypeComboBox.setMaxWidth(170);
comboBox.setMaxWidth(220);
HelpLabel helpLabel = new 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);
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);
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
@ -133,7 +144,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
showHideLink.setVisible(true);
setExpanded(false);
try {
importWallet(scriptTypeComboBox.getValue());
importWallet(comboBox.getValue());
} catch(ImportException e) {
log.error("Error importing file", e);
String errorMessage = e.getMessage();
@ -154,8 +165,14 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
contentBox.setPadding(new Insets(10, 30, 10, 30));
contentBox.setPrefHeight(60);
Platform.runLater(scriptTypeComboBox::requestFocus);
Platform.runLater(comboBox::requestFocus);
return contentBox;
}
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
public String getDescription() {
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
}
}
}

View File

@ -25,8 +25,6 @@ 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;
@ -162,6 +160,17 @@ 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();
@ -297,8 +306,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
private void checkWalletSigning(Wallet wallet) {
if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
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");
}
}
@ -323,7 +332,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private boolean isValidAddress() {
try {
Address address = getAddress();
return address.getScriptType().isAllowed(PolicyType.SINGLE) || address.getScriptType() == ScriptType.P2SH;
return address.getScriptType().isAllowed(PolicyType.SINGLE_HD) || address.getScriptType() == ScriptType.P2SH;
} catch (InvalidAddressException e) {
return false;
}
@ -379,18 +388,24 @@ 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(isBip322()) {
ScriptType scriptType = decryptedWallet.getScriptType();
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
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();
} else {
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
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();
}
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());
@ -466,11 +481,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
if(scriptType == ScriptType.P2SH) {
scriptType = ScriptType.P2SH_P2WPKH;
}
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).contains(scriptType)) {
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).contains(scriptType)) {
throw new IllegalArgumentException("Only single signature P2PKH, P2SH-P2WPKH or P2WPKH addresses can verify messages.");
}
Address signedMessageAddress = scriptType.getAddress(signedMessageKey);
Address signedMessageAddress = scriptType.getAddress(PolicyType.SINGLE_HD, signedMessageKey);
return providedAddress.equals(signedMessageAddress);
}
@ -500,12 +515,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private void showBip322Qr() {
Wallet signingWallet = walletNode.getWallet();
ScriptType scriptType = signingWallet.getScriptType();
PSBT psbt = buildBip322Psbt(signingWallet);
PSBT psbt = Bip322.getBip322Psbt(scriptType, walletNode.getAddress(), message.getText().trim());
addBip322DerivationInfo(psbt, signingWallet);
byte[] psbtBytes = psbt.serialize();
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);
@ -516,6 +528,40 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
}
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);
@ -527,7 +573,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
psbtInput.setTapInternalKey(pubKey);
psbtInput.getTapDerivedPublicKeys().put(ECKey.fromPublicOnly(pubKey.getPubKeyXCoord()), Map.of(fullDerivation, Collections.emptyList()));
} else {
psbtInput.getDerivedPublicKeys().put(scriptType.getOutputKey(pubKey), fullDerivation);
psbtInput.getDerivedPublicKeys().put(scriptType.getOutputKey(signingWallet.getPolicyType(), pubKey), fullDerivation);
}
}
@ -539,11 +585,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
QRScanDialog.Result result = optionalResult.get();
if(result.psbt != null) {
try {
Wallet signingWallet = walletNode.getWallet();
ECKey pubKey = signingWallet.getKeystores().get(0).getPubKey(walletNode);
String sig = Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), result.psbt, pubKey);
signature.clear();
signature.appendText(sig);
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());
@ -601,9 +647,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private void exportBip322File() {
Wallet signingWallet = walletNode.getWallet();
ScriptType scriptType = signingWallet.getScriptType();
PSBT psbt = Bip322.getBip322Psbt(scriptType, walletNode.getAddress(), message.getText().trim());
addBip322DerivationInfo(psbt, signingWallet);
PSBT psbt = buildBip322Psbt(signingWallet);
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
@ -613,7 +657,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
File file = fileChooser.showSaveDialog(window);
if(file != null) {
try(OutputStream os = new FileOutputStream(file)) {
os.write(psbt.serialize());
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());
@ -644,10 +688,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
try {
byte[] psbtBytes = Files.readAllBytes(file.toPath());
PSBT signedPsbt = new PSBT(psbtBytes, false);
ECKey pubKey = walletNode.getWallet().getKeystores().get(0).getPubKey(walletNode);
String sig = Bip322.getBip322SignatureFromPsbt(walletNode.getWallet().getScriptType(), signedPsbt, pubKey);
signature.clear();
signature.appendText(sig);
String sig = extractBip322Signature(signedPsbt);
if(sig != null) {
signature.clear();
signature.appendText(sig);
}
return;
} catch(Exception e) {
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt")) {

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.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
importer.getKeystore(wallet.getPolicyType(), 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(derivation, wordEntriesProperty.get(), passphraseProperty.get());
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), 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.getScriptType().getDefaultDerivation(), existing, passphraseProperty.get());
importer.getKeystore(wallet.getPolicyType(), 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(derivation, mnemonicShares, passphraseProperty.get());
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), 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(ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
importer.getKeystore(PolicyType.SINGLE_HD, 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).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).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)) {
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD)) {
for(List<ChildNumber> derivation : derivations) {
try {
Wallet wallet = getWallet(scriptType, derivation);
Wallet wallet = getWallet(PolicyType.SINGLE_HD, 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 a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
"Could not find an HD 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,41 +163,49 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
walletDiscoveryService.start();
}
private Wallet getWallet(ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
private Wallet getWallet(PolicyType policyType, ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
Wallet wallet = new Wallet("");
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setPolicyType(policyType);
wallet.setScriptType(scriptType);
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
Keystore keystore = importer.getKeystore(policyType, derivation, wordEntriesProperty.get(), passphraseProperty.get());
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), 1));
return wallet;
}
private Node getScriptTypeEntry() {
Label label = new Label("Script Type:");
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));
}
}
HBox fieldBox = new HBox(5);
fieldBox.setAlignment(Pos.CENTER_RIGHT);
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
if(scriptTypeComboBox.getItems().contains(ScriptType.P2WPKH)) {
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
ComboBox<PolicyAndScriptType> comboBox = new ComboBox<>(FXCollections.observableArrayList(types));
PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH);
if(types.contains(defaultType)) {
comboBox.setValue(defaultType);
}
scriptTypeComboBox.setConverter(new StringConverter<>() {
comboBox.setConverter(new StringConverter<>() {
@Override
public String toString(ScriptType scriptType) {
return scriptType == null ? "" : scriptType.getDescription();
public String toString(PolicyAndScriptType type) {
return type == null ? "" : type.getDescription();
}
@Override
public ScriptType fromString(String string) {
public PolicyAndScriptType fromString(String string) {
return null;
}
});
scriptTypeComboBox.setMaxWidth(170);
comboBox.setMaxWidth(220);
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.\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);
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);
Region region = new Region();
HBox.setHgrow(region, Priority.SOMETIMES);
@ -208,8 +216,8 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
showHideLink.setVisible(true);
setExpanded(false);
try {
ScriptType scriptType = scriptTypeComboBox.getValue();
Wallet wallet = getWallet(scriptType, scriptType.getDefaultDerivation());
PolicyAndScriptType type = comboBox.getValue();
Wallet wallet = getWallet(type.policyType(), type.scriptType(), type.scriptType().getDefaultDerivation());
EventManager.get().post(new WalletImportEvent(wallet));
} catch(ImportException e) {
log.error("Error importing mnemonic", e);
@ -231,4 +239,10 @@ 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,6 +14,8 @@ 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;
@ -66,6 +68,7 @@ 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();
@ -109,7 +112,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)));
keyScriptType.setItems(FXCollections.observableList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD)));
keyScriptTypeField.getInputs().add(keyScriptType);
keyScriptType.setConverter(new StringConverter<ScriptType>() {
@ -204,18 +207,31 @@ 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) {
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
if(selectedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
toAddress.setText(selectedWallet.getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
} else {
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
}
}
});
keyScriptType.setValue(ScriptType.P2PKH);
if(wallet != null) {
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
toAddress.setText(wallet.getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
} else {
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
}
}
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));
@ -272,10 +288,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
}
private boolean isValidToAddress() {
try {
Address address = getToAddress();
if(silentPaymentAddress != null) {
return true;
} catch (InvalidAddressException e) {
}
try {
getToAddress();
return true;
} catch(InvalidAddressException e) {
return false;
}
}
@ -287,14 +306,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
private void setFromAddress() {
DumpedPrivateKey privateKey = getPrivateKey();
ScriptType scriptType = keyScriptType.getValue();
Address address = scriptType.getAddress(privateKey.getKey());
Address address = scriptType.getAddress(PolicyType.SINGLE_HD, 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))) {
keyScriptType.getItems().addAll(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE).stream().filter(s -> !keyScriptType.getItems().contains(s)).collect(Collectors.toList()));
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()));
} else if(!compressed && !keyScriptType.getItems().equals(List.of(ScriptType.P2PKH))) {
keyScriptType.getSelectionModel().select(0);
keyScriptType.getItems().removeIf(scriptType -> scriptType != ScriptType.P2PKH);
@ -346,8 +365,9 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
try {
DumpedPrivateKey privateKey = getPrivateKey();
ScriptType scriptType = keyScriptType.getValue();
Address fromAddress = scriptType.getAddress(privateKey.getKey());
Address destAddress = getToAddress();
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);
Date since = null;
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
@ -363,7 +383,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress, since);
addressUtxosService.setOnSucceeded(successEvent -> {
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), payment);
});
addressUtxosService.setOnFailed(failedEvent -> {
Throwable rootCause = Throwables.getRootCause(failedEvent.getSource().getException());
@ -383,13 +403,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
}
}
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Address destAddress) {
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();
ECKey pubKey = ECKey.fromPublicOnly(privKey);
Transaction noFeeTransaction = new Transaction();
long total = 0;
for(TransactionOutput txOutput : txOutputs) {
scriptType.addSpendingInput(noFeeTransaction, txOutput, pubKey, TransactionSignature.dummy(scriptType == P2TR ? TransactionSignature.Type.SCHNORR : TransactionSignature.Type.ECDSA));
scriptType.addSpendingInput(PolicyType.SINGLE_HD, noFeeTransaction, txOutput, pubKey, TransactionSignature.dummy(scriptType == P2TR ? TransactionSignature.Type.SCHNORR : TransactionSignature.Type.ECDSA));
total += txOutput.getValue();
}
@ -448,7 +469,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
psbtInput.setWitnessScript(txInput.getWitness().getWitnessScript());
}
if(!psbtInput.sign(scriptType.getOutputKey(privKey))) {
if(!psbtInput.sign(scriptType.getOutputKey(PolicyType.SINGLE_HD, privKey))) {
AppServices.showErrorDialog("Failed to sign", "Failed to sign for transaction output " + utxoOutput.getHash() + ":" + utxoOutput.getIndex());
return;
}
@ -456,7 +477,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
Transaction finalizeTransaction = new Transaction();
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
TransactionInput finalizedTxInput = scriptType.addSpendingInput(PolicyType.SINGLE_HD, finalizeTransaction, utxoOutput, pubKey, signature);
psbtInput.setFinalScriptSig(finalizedTxInput.getScriptSig());
psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness());
}
@ -468,6 +489,29 @@ 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,6 +51,7 @@ 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;
@ -582,6 +583,14 @@ 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());
@ -679,11 +688,16 @@ 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());
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
if(urhdKey.getName() != null) {
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
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());
}
}
} 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,6 +28,8 @@ 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;
@ -35,20 +37,26 @@ 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, List<Payment> payments) {
public SendToManyDialog(BitcoinUnit bitcoinUnit, UnitFormat unitFormat, 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);
@ -119,11 +127,9 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
addressCell.getStyleClass().add("fixed-width");
list.add(addressCell);
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);
long rawAmount = sendToPayment.payment().getAmount();
Double amount = rawAmount < 0 ? null : bitcoinUnit.getValue(rawAmount);
SpreadsheetCell amountCell = amountCellType.createCell(row, 1, 1, 1, amount);
amountCell.setFormat(bitcoinUnit == BitcoinUnit.BTC ? "0.00000000" : "###,###");
amountCell.getStyleClass().add("number-value");
if(OsType.getCurrent() == OsType.MACOS) {
@ -177,7 +183,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.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
if(sendToAddress != null && sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
return true;
}
}
@ -216,12 +222,15 @@ 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) {
double doubleAmount = Double.parseDouble(csvReader.get(1).replace(",", ""));
amount = (long)(doubleAmount * Transaction.SATOSHIS_PER_BITCOIN);
String normalised = groupingStripped.replaceAll(Pattern.quote(unitFormat.getDecimalSeparator()), ".");
double doubleAmount = Double.parseDouble(normalised);
amount = bitcoinUnit.getSatsValue(doubleAmount);
} else {
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
amount = Long.parseLong(groupingStripped);
}
String label = csvReader.get(2);
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
@ -359,6 +368,160 @@ 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;
@ -487,11 +650,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
}
if(sendToAddress != null && value != null) {
if(bitcoinUnit == BitcoinUnit.BTC) {
value = value * Transaction.SATOSHIS_PER_BITCOIN;
}
payments.add(sendToAddress.toPayment(label, value.longValue(), false));
payments.add(sendToAddress.toPayment(label, bitcoinUnit.getSatsValue(value), false));
}
}

View File

@ -7,6 +7,7 @@ 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;
@ -228,7 +229,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(), walletTx.getPayments().size() + 2);
int maxRows = Math.max(maxSetSize * utxoSets.size(), diagram.getDisplayedOutputs().size() + 1);
double diagramHeight = Math.max(DIAGRAM_HEIGHT, Math.min(EXPANDED_DIAGRAM_HEIGHT, maxRows * ROW_HEIGHT));
diagram.setMinHeight(diagramHeight);
diagram.setMaxHeight(diagramHeight);
@ -256,12 +257,12 @@ public class TransactionDiagram extends GridPane {
Pane txPane = getTransactionPane();
GridPane.setConstraints(txPane, 3, 0);
List<Payment> displayedPayments = getDisplayedPayments();
List<WalletTransaction.Output> displayedOutputs = getDisplayedOutputs();
Pane outputsLinesPane = getOutputsLines(displayedPayments);
Pane outputsLinesPane = getOutputsLines(displayedOutputs);
GridPane.setConstraints(outputsLinesPane, 4, 0);
Pane outputsPane = getOutputsLabels(displayedPayments);
Pane outputsPane = getOutputsLabels(displayedOutputs);
GridPane.setConstraints(outputsPane, 5, 0);
getChildren().clear();
@ -652,33 +653,48 @@ public class TransactionDiagram extends GridPane {
return value * (1.0 - scaleFactor) + additional;
}
private List<Payment> getDisplayedPayments() {
List<Payment> payments = walletTx.getPayments();
private List<WalletTransaction.Output> getDisplayedOutputs() {
List<WalletTransaction.Output> outputs = walletTx.getOutputs().stream().filter(o -> !(o instanceof WalletTransaction.NonAddressOutput)).toList();
int maxPayments = getMaxPayments();
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(payment);
}
}
long paginableCount = outputs.stream().filter(this::isPaymentAndNotChange).count();
displayedPayments.add(new AdditionalPayment(additional));
return displayedPayments;
} else {
return payments;
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();
} else {
additional.add(output instanceof WalletTransaction.PaymentOutput po ? po.getPayment() : ((WalletTransaction.ConsolidationOutput)output).getWalletNodePayment());
}
} else {
displayedOutputs.add(output);
}
}
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<Payment> displayedPayments) {
private Pane getOutputsLines(List<WalletTransaction.Output> displayedOutputs) {
VBox pane = new VBox();
Group group = new Group();
VBox.setVgrow(group, Priority.ALWAYS);
@ -693,10 +709,9 @@ public class TransactionDiagram extends GridPane {
double width = 140.0;
long sum = walletTx.getTotal();
List<Long> values = walletTx.getOutputs().stream().filter(output -> !(output instanceof WalletTransaction.NonAddressOutput))
.map(output -> output.getTransactionOutput().getValue()).collect(Collectors.toList());
List<Long> values = displayedOutputs.stream().map(o -> o.getTransactionOutput().getValue()).collect(Collectors.toList());
values.add(walletTx.getFee());
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
int numOutputs = displayedOutputs.size() + 1;
for(int i = 1; i <= numOutputs; i++) {
CubicCurve curve = new CubicCurve();
curve.getStyleClass().add("output-line");
@ -728,121 +743,21 @@ public class TransactionDiagram extends GridPane {
return pane;
}
private Pane getOutputsLabels(List<Payment> displayedPayments) {
private Pane getOutputsLabels(List<WalletTransaction.Output> displayedOutputs) {
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(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));
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));
}
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) {
@ -889,6 +804,143 @@ 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));
@ -970,8 +1022,8 @@ public class TransactionDiagram extends GridPane {
}
private String getDiagramTitle() {
if(!isFinal() && walletTx.getPayments().size() > 0 && walletTx.getPayments().get(0).getLabel() != null) {
return walletTx.getPayments().get(0).getLabel();
if(!isFinal() && !walletTx.getPayments().isEmpty() && walletTx.getPayments().getFirst().getLabel() != null) {
return walletTx.getPayments().getFirst().getLabel();
} else {
return "[" + walletTx.getTransaction().getTxId().toString().substring(0, 6) + "]";
}
@ -1023,15 +1075,6 @@ 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,22 +96,38 @@ public class TransactionDiagramLabel extends HBox {
outputLabels.add(remixOutputLabel);
}
} else {
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)) {
paymentLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
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));
}
}
}
}
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()));
if(walletTx.getSelectedUtxos().values().stream().allMatch(Objects::isNull)) {
externalLabels.sort(Comparator.comparingInt(paymentLabel -> (paymentLabel.text.startsWith("Receive") ? 0 : 1)));
}
outputLabels.addAll(externalLabels);
outputLabels.addAll(consolidationLabels);
outputLabels.addAll(mixLabels);
}
Map<WalletNode, Long> changeMap = walletTx.getChangeMap();
outputLabels.addAll(changeMap.entrySet().stream().map(changeEntry -> getOutputLabel(transactionDiagram, changeEntry)).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));
}
}
OutputLabel feeOutputLabel = getFeeOutputLabel(transactionDiagram);
if(feeOutputLabel != null) {
@ -200,22 +216,31 @@ public class TransactionDiagramLabel extends HBox {
return getOutputLabel(glyph, text);
}
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Payment payment) {
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, WalletTransaction.Output output) {
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(transactionDiagram.getWalletTransaction(), payment);
String text = (toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ") + transactionDiagram.getCoinValue(payment.getAmount()) + " to " + payment;
Glyph glyph = GlyphUtils.getOutputGlyph(walletTx, payment);
String text = (toNode != null || spConsolidation ? "Consolidate " : (toWallet == null ? "Pay " : "Receive ")) + transactionDiagram.getCoinValue(payment.getAmount()) + " to " + payment;
return getOutputLabel(glyph, text);
}
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, Map.Entry<WalletNode, Long> changeEntry) {
private OutputLabel getOutputLabel(TransactionDiagram transactionDiagram, WalletTransaction.ChangeOutput changeOutput) {
WalletTransaction walletTx = transactionDiagram.getWalletTransaction();
Glyph glyph = GlyphUtils.getChangeGlyph();
String text = "Change of " + transactionDiagram.getCoinValue(changeEntry.getValue()) + " to " + walletTx.getChangeAddress(changeEntry.getKey()).toString();
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();
return getOutputLabel(glyph, text);
}

View File

@ -44,11 +44,13 @@ public class WalletExportDialog extends Dialog<Wallet> {
AnchorPane.setRightAnchor(scrollPane, 0.0);
List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) {
if(wallet.getPolicyType() == PolicyType.SINGLE_HD) {
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) {
} else if(wallet.getPolicyType() == PolicyType.MULTI_HD) {
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,5 +1,6 @@
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;
@ -48,9 +49,13 @@ 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 && (Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
boolean requestBirthDate = !rename && (walletPolicyType == PolicyType.SINGLE_SP || Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
setTitle("Wallet Name");
dialogPane.setHeaderText("Enter a name for this wallet:");

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(derivation, xprv);
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), 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, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, KEYSTORE_SP_SCAN, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
}
}

View File

@ -0,0 +1,22 @@
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

@ -0,0 +1,24 @@
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

@ -0,0 +1,21 @@
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

@ -0,0 +1,9 @@
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,7 +1,6 @@
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;
@ -16,7 +15,7 @@ public class GlyphUtils {
return getFakeMixGlyph();
} else if(payment.getType().equals(Payment.Type.ANCHOR)) {
return getAnchorGlyph();
} else if(payment instanceof WalletNodePayment) {
} else if(walletTx.isConsolidation(payment)) {
return getConsolidationGlyph();
} else if(walletTx.isPremixSend(payment)) {
return getPremixGlyph();

View File

@ -4,6 +4,7 @@ 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;
@ -93,7 +94,7 @@ public class Bip129 implements KeystoreFileExport, KeystoreFileImport, WalletExp
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
if(password != null) {
@ -235,6 +236,10 @@ 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,6 +2,7 @@ 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;
@ -25,10 +26,10 @@ public class Bip32 implements KeystoreXprvImport {
}
@Override
public Keystore getKeystore(List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException {
public Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException {
try {
MasterPrivateExtendedKey masterPrivateExtendedKey = new MasterPrivateExtendedKey(xprv.getKey().getPrivKeyBytes(), xprv.getKey().getChainCode());
return Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, derivation);
return Keystore.fromMasterPrivateExtendedKey(masterPrivateExtendedKey, policyType, derivation);
} catch(Exception e) {
throw new ImportException(e);
}

View File

@ -1,6 +1,7 @@
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;
@ -22,11 +23,11 @@ public class Bip39 implements KeystoreMnemonicImport {
}
@Override
public Keystore getKeystore(List<ChildNumber> derivation, List<String> mnemonicWords, String passphrase) throws ImportException {
public Keystore getKeystore(PolicyType policyType, 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, derivation);
return Keystore.fromSeed(seed, policyType, derivation);
} catch (Exception e) {
try {
ElectrumMnemonicCode.INSTANCE.check(mnemonicWords);

View File

@ -3,6 +3,7 @@ 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;
@ -28,12 +29,12 @@ public class Bip93 implements KeystoreCodexImport {
}
@Override
public Keystore getKeystore(List<ChildNumber> derivation, String secretShare) throws ImportException {
public Keystore getKeystore(PolicyType policyType, 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, derivation);
Keystore keystore = Keystore.fromMasterPrivateExtendedKey(mpek, policyType, 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);
wallet.setPolicyType(PolicyType.MULTI_HD);
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, scriptType, wallet.getKeystores(), cf.quorum.requiredSigners));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI_HD, 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)) {
if(!wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
throw new ExportException(getName() + " import requires a multisig wallet");
}

View File

@ -5,6 +5,7 @@ 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;
@ -107,7 +108,7 @@ public abstract class CardApi {
public abstract Service<Void> getInitializationService(byte[] entropy, StringProperty messageProperty);
public abstract Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty);
public abstract Service<Keystore> getImportService(PolicyType policyType, List<ChildNumber> derivation, StringProperty messageProperty);
public abstract Service<PSBT> getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty);

View File

@ -1,5 +1,6 @@
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;
@ -19,10 +20,9 @@ public class CoboVaultMultisig extends ColdcardMultisig {
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
keystore.setLabel("Cobo Vault");
keystore.setWalletModel(getWalletModel());
return keystore;
}

View File

@ -35,7 +35,11 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
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");
}
try {
Gson gson = new Gson();
CoboVaultSinglesigKeystore coboKeystore = gson.fromJson(new InputStreamReader(inputStream, StandardCharsets.UTF_8), CoboVaultSinglesigKeystore.class);
@ -47,7 +51,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
Keystore keystore = new Keystore();
keystore.setLabel(getName());
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(WalletModel.COBO_VAULT);
keystore.setWalletModel(getWalletModel());
keystore.setKeyDerivation(new KeyDerivation(coboKeystore.MasterFingerprint.toLowerCase(Locale.ROOT), "m/" + coboKeystore.AccountKeyPath, true));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(coboKeystore.ExtPubKey));
@ -70,13 +74,13 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
//Use default of P2WPKH
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
Keystore keystore = getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH, inputStream, "");
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setPolicyType(PolicyType.SINGLE_HD);
wallet.setScriptType(ScriptType.P2WPKH);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, 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(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(PolicyType policyType, 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(scriptType, firstClone, password);
keystore = getKeystoreMultisig(policyType, scriptType, firstClone, password);
} catch(Exception e) {
keystore = getKeystoreSinglesig(scriptType, secondClone, password);
keystore = getKeystoreSinglesig(policyType, scriptType, secondClone, password);
}
return keystore;
@ -52,18 +52,18 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
}
}
private Keystore getKeystoreSinglesig(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
private Keystore getKeystoreSinglesig(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
ColdcardSinglesig coldcardSinglesig = new ColdcardSinglesig();
return coldcardSinglesig.getKeystore(scriptType, inputStream, password);
return coldcardSinglesig.getKeystore(policyType, scriptType, inputStream, password);
}
public Keystore getKeystoreMultisig(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystoreMultisig(PolicyType policyType, 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(WalletModel.COLDCARD);
keystore.setWalletModel(getWalletModel());
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);
wallet.setPolicyType(PolicyType.MULTI_HD);
int threshold = 2;
ScriptType scriptType = ScriptType.P2SH;
@ -167,7 +167,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
}
Policy policy = Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), threshold);
Policy policy = Policy.getPolicy(PolicyType.MULTI_HD, 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)) {
if(!wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
throw new ExportException(getName() + " import requires a multisig wallet");
}

View File

@ -8,6 +8,7 @@ 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;
@ -54,21 +55,37 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(PolicyType policyType, 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("File was not a valid " + getName() + " wallet export");
if(map.get("xfp") == null) {
throw new ImportException("Export was not a valid " + getName() + " wallet export");
}
String masterFingerprint = map.get("xfp").getAsString();
for (String key : map.keySet()) {
if (key.startsWith("bip")) {
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")) {
ColdcardKeystore ck = gson.fromJson(map.get(key), ColdcardKeystore.class);
if(ck.name != null) {
@ -77,7 +94,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
Keystore keystore = new Keystore();
keystore.setLabel(getName());
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(WalletModel.COLDCARD);
keystore.setWalletModel(getWalletModel());
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, ck.deriv, true));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(ck.xpub));
@ -86,7 +103,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
}
}
}
} catch (Exception e) {
} catch(Exception e) {
throw new ImportException("Error getting " + getName() + " keystore", e);
}
@ -101,13 +118,13 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
//Use default of P2WPKH
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
Keystore keystore = getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH, inputStream, "");
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setPolicyType(PolicyType.SINGLE_HD);
wallet.setScriptType(ScriptType.P2WPKH);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, ScriptType.P2WPKH, wallet.getKeystores(), null));
try {
wallet.checkWallet();
@ -122,6 +139,7 @@ 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);
@ -64,6 +64,7 @@ public class Config {
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;
@ -448,6 +449,10 @@ public class Config {
return dustAttackThreshold;
}
public long getDustAttackThresholdSp() {
return dustAttackThresholdSp;
}
public int getEnumerateHwPeriod() {
return enumerateHwPeriod;
}
@ -556,13 +561,6 @@ 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,26 +29,43 @@ 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));
bufferedWriter.write("# Receive and change descriptor (BIP389):");
bufferedWriter.newLine();
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null);
bufferedWriter.write(outputDescriptor.toString(true));
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.newLine();
bufferedWriter.write("# Single argument descriptor:");
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.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.flush();
} catch(Exception e) {

View File

@ -40,10 +40,10 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Wallet wallet = importWallet(inputStream, password);
if(!wallet.getPolicyType().equals(PolicyType.SINGLE) || wallet.getKeystores().size() != 1) {
if(wallet.getPolicyType().equals(PolicyType.MULTI_HD) || 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);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
wallet.setPolicyType(PolicyType.SINGLE_HD);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, scriptType, wallet.getKeystores(), 1));
} else if(ew.wallet_type.contains("of")) {
wallet.setPolicyType(PolicyType.MULTI);
wallet.setPolicyType(PolicyType.MULTI_HD);
String[] mOfn = ew.wallet_type.split("of");
int threshold = Integer.parseInt(mOfn[0]);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), threshold));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI_HD, 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)) {
if(wallet.getPolicyType().equals(PolicyType.SINGLE_HD)) {
ew.wallet_type = "standard";
} else if(wallet.getPolicyType().equals(PolicyType.MULTI)) {
} else if(wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
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)) {
if(wallet.getPolicyType().equals(PolicyType.SINGLE_HD)) {
ew.keystores.put("keystore", ek);
} else if(wallet.getPolicyType().equals(PolicyType.MULTI)) {
} else if(wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
ew.keystores.put("x" + index + "/", ek);
}

View File

@ -30,6 +30,10 @@ 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.");
}
@ -61,7 +65,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) {
if(wallet.getPolicyType() == PolicyType.MULTI_HD) {
writer.write(wallet.getDefaultPolicy().getNumSignaturesRequired() + " ");
}

View File

@ -1,6 +1,7 @@
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,7 +26,7 @@ public class GordianSeedTool implements KeystoreFileImport {
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
throw new ImportException("Only QR imports are supported.");
}

View File

@ -6,6 +6,7 @@ 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;
@ -145,7 +146,7 @@ public class Hwi {
}
}
public Map<WalletType, String> getXpubs(Device device, String passphrase, Map<WalletType, String> accountDerivationPaths, Map<WalletType, String> accountXpubs) throws ImportException {
public Map<WalletType, ExtendedKey> getXpubs(Device device, String passphrase, Map<WalletType, String> accountDerivationPaths, Map<WalletType, ExtendedKey> accountXpubs) throws ImportException {
for(Map.Entry<WalletType, String> entry : accountDerivationPaths.entrySet()) {
accountXpubs.put(entry.getKey(), getXpub(device, passphrase, entry.getValue()));
}
@ -153,12 +154,12 @@ public class Hwi {
return accountXpubs;
}
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
public ExtendedKey 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.toString();
return xpub;
} catch(DeviceException e) {
throw new ImportException(e.getMessage(), e);
} catch(RuntimeException e) {
@ -167,6 +168,20 @@ 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 {
@ -421,7 +436,7 @@ public class Hwi {
}
}
public static class GetXpubService extends Service<String> {
public static class GetXpubService extends Service<ExtendedKey> {
private final Device device;
private final String passphrase;
private final String derivationPath;
@ -433,9 +448,9 @@ public class Hwi {
}
@Override
protected Task<String> createTask() {
protected Task<ExtendedKey> createTask() {
return new Task<>() {
protected String call() throws ImportException {
protected ExtendedKey call() throws ImportException {
Hwi hwi = new Hwi();
return hwi.getXpub(device, passphrase, derivationPath);
}
@ -443,7 +458,29 @@ public class Hwi {
}
}
public static class GetXpubsService extends Service<Map<WalletType, String>> {
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>> {
private final Device device;
private final String passphrase;
private final Map<WalletType, String> accountDerivationPaths;
@ -455,13 +492,13 @@ public class Hwi {
}
@Override
protected Task<Map<WalletType, String>> createTask() {
protected Task<Map<WalletType, ExtendedKey>> createTask() {
return new Task<>() {
protected Map<WalletType, String> call() throws ImportException {
protected Map<WalletType, ExtendedKey> call() throws ImportException {
Hwi hwi = new Hwi();
updateProgress(0, accountDerivationPaths.size());
ObservableMap<WalletType, String> accountXpubs = FXCollections.observableMap(new LinkedHashMap<>());
accountXpubs.addListener((MapChangeListener<? super WalletType, ? super String>) _ -> updateProgress(accountXpubs.size(), accountDerivationPaths.size()));
ObservableMap<WalletType, ExtendedKey> accountXpubs = FXCollections.observableMap(new LinkedHashMap<>());
accountXpubs.addListener((MapChangeListener<? super WalletType, ? super ExtendedKey>) _ -> updateProgress(accountXpubs.size(), accountDerivationPaths.size()));
return hwi.getXpubs(device, passphrase, accountDerivationPaths, accountXpubs);
}
};

View File

@ -1,5 +1,6 @@
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,7 +25,7 @@ public class Jade implements KeystoreFileImport {
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
throw new ImportException("Failed to detect a valid " + scriptType.getDescription() + " keystore.");
}

View File

@ -1,5 +1,6 @@
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;
@ -63,14 +64,13 @@ 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(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
throw new ImportException("Failed to detect a valid " + scriptType.getDescription() + " keystore.");
}

View File

@ -12,6 +12,7 @@ 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;
@ -337,6 +338,8 @@ 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());
@ -453,6 +456,30 @@ 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,5 +1,6 @@
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;
@ -23,10 +24,9 @@ public class KeycardShellMultisig extends ColdcardMultisig {
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
keystore.setLabel("Keycard Shell");
keystore.setWalletModel(getWalletModel());
return keystore;
}

View File

@ -1,6 +1,7 @@
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;
@ -24,10 +25,9 @@ public class KeycardShellSinglesig extends KeystoneSinglesig {
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
keystore.setLabel("Keycard Shell");
keystore.setWalletModel(getWalletModel());
return keystore;
}

View File

@ -1,5 +1,6 @@
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;
@ -19,10 +20,9 @@ public class KeystoneMultisig extends ColdcardMultisig {
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
keystore.setLabel("Keystone");
keystore.setWalletModel(getWalletModel());
return keystore;
}

View File

@ -1,6 +1,5 @@
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;
@ -37,7 +36,7 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
public Keystore getKeystore(PolicyType policyType, 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);
@ -46,24 +45,22 @@ 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");
}
ExtendedKey xpub = descriptor.getSingletonExtendedPublicKey();
KeyDerivation keyDerivation = descriptor.getKeyDerivation(xpub);
Keystore keystore = new Keystore();
Wallet wallet = descriptor.toWallet();
Keystore keystore = wallet.getKeystores().getFirst();
keystore.setLabel(getName());
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
keystore.setWalletModel(WalletModel.KEYSTONE);
keystore.setKeyDerivation(keyDerivation);
keystore.setExtendedPublicKey(xpub);
keystore.setWalletModel(getWalletModel());
return keystore;
} catch (IllegalArgumentException e) {
throw new ImportException("Error getting " + getName() + " keystore - not an output descriptor", e);
} catch (Exception e) {
} catch(Exception e) {
throw new ImportException("Error getting " + getName() + " keystore", e);
}
}
@ -76,13 +73,13 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
//Use default of P2WPKH
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
Keystore keystore = getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH, inputStream, "");
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.SINGLE);
wallet.setPolicyType(PolicyType.SINGLE_HD);
wallet.setScriptType(ScriptType.P2WPKH);
wallet.getKeystores().add(keystore);
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, ScriptType.P2WPKH, wallet.getKeystores(), null));
try {
wallet.checkWallet();

View File

@ -1,12 +1,13 @@
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(String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException;
Keystore getKeystore(PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException;
String getKeystoreImportDescription(int account);
}

View File

@ -1,10 +1,11 @@
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(List<ChildNumber> derivation, String secretShare) throws ImportException;
Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, String secretShare) throws ImportException;
}

View File

@ -1,12 +1,13 @@
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(ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
String getKeystoreImportDescription(int account);
default String getKeystoreImportDescription() {
return getKeystoreImportDescription(0);

View File

@ -1,10 +1,11 @@
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(List<ChildNumber> derivation, List<String> mnemonicWords, String passphrase) throws ImportException;
Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, List<String> mnemonicWords, String passphrase) throws ImportException;
}

View File

@ -1,10 +1,11 @@
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(List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException;
Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException;
}

View File

@ -2,10 +2,11 @@ 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(List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException;
Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, ExtendedKey xprv) throws ImportException;
}

View File

@ -1,5 +1,6 @@
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;
@ -19,10 +20,9 @@ public class PassportMultisig extends ColdcardMultisig {
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
keystore.setLabel("Passport");
keystore.setWalletModel(getWalletModel());
return keystore;
}

View File

@ -1,5 +1,6 @@
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;
@ -18,10 +19,9 @@ public class PassportSinglesig extends ColdcardSinglesig {
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
public Keystore getKeystore(PolicyType policyType, ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
Keystore keystore = super.getKeystore(policyType, scriptType, inputStream, password);
keystore.setLabel("Passport");
keystore.setWalletModel(getWalletModel());
return keystore;
}

View File

@ -7,6 +7,7 @@ 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.*;
@ -25,7 +26,11 @@ public class Samourai implements KeystoreFileImport {
}
@Override
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
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");
}
try {
String input = CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
@ -52,7 +57,7 @@ public class Samourai implements KeystoreFileImport {
SamouraiBackup backup = new Gson().fromJson(decrypted, SamouraiBackup.class);
DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes(backup.wallet.seed), password, 0);
Keystore keystore = Keystore.fromSeed(seed, scriptType.getDefaultDerivation());
Keystore keystore = Keystore.fromSeed(seed, PolicyType.SINGLE_HD, scriptType.getDefaultDerivation());
keystore.setLabel(getWalletModel().toDisplayString());
return keystore;
} catch(JsonParseException e) {

View File

@ -1,6 +1,7 @@
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;
@ -25,7 +26,7 @@ public class Slip39 implements KeystoreMnemonicShareImport {
}
@Override
public Keystore getKeystore(List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException {
public Keystore getKeystore(PolicyType policyType, List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException {
try {
RecoveryState recoveryState = new RecoveryState();
for(List<String> mnemonicWords : mnemonicShares) {
@ -36,7 +37,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, derivation);
return Keystore.fromSeed(seed, policyType, derivation);
} else {
throw new Slip39ProgressException(recoveryState.getShortStatus(), recoveryState.getStatus());
}

View File

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

View File

@ -2,6 +2,7 @@ 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;
@ -182,18 +183,20 @@ public class Storage {
Keystore keystore = wallet.getKeystores().get(i);
if(keystore.hasSeed()) {
Keystore copyKeystore = copy.getKeystores().get(i);
Keystore derivedKeystore = Keystore.fromSeed(copyKeystore.getSeed(), copyKeystore.getKeyDerivation().getDerivation());
Keystore derivedKeystore = Keystore.fromSeed(copyKeystore.getSeed(), wallet.getPolicyType(), 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(), copyKeystore.getKeyDerivation().getDerivation());
Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), wallet.getPolicyType(), copyKeystore.getKeyDerivation().getDerivation());
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
keystore.setSilentPaymentScanAddress(derivedKeystore.getSilentPaymentScanAddress());
copyKeystore.getMasterPrivateKey().clear();
}
}

View File

@ -6,6 +6,7 @@ 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;
@ -68,7 +69,11 @@ public class WalletLabels implements WalletImport, WalletExport {
for(Keystore keystore : exportWallet.getKeystores()) {
if(keystore.getLabel() != null && !keystore.getLabel().isEmpty()) {
labels.add(new Label(Type.xpub, keystore.getExtendedPublicKey().toString(), keystore.getLabel(), null, null));
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));
}
}
}
@ -220,7 +225,17 @@ public class WalletLabels implements WalletImport, WalletExport {
if(label.type == Type.xpub) {
for(Keystore keystore : wallet.getKeystores()) {
if(keystore.getExtendedPublicKey().toString().equals(label.ref)) {
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)) {
keystore.setLabel(label.label);
List<Keystore> changedKeystores = changedWalletKeystores.computeIfAbsent(wallet, w -> new ArrayList<>());
changedKeystores.add(keystore);
@ -432,7 +447,7 @@ public class WalletLabels implements WalletImport, WalletExport {
}
private enum Type {
tx, addr, pubkey, input, output, xpub
tx, addr, pubkey, input, output, xpub, spscan
}
private static class Label {

View File

@ -10,15 +10,36 @@ 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,6 +6,7 @@ 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;
@ -141,12 +142,12 @@ public class CkCardApi extends CardApi {
}
@Override
public Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty) {
public Service<Keystore> getImportService(PolicyType policyType, List<ChildNumber> derivation, StringProperty messageProperty) {
if(cardType == WalletModel.SATSCHIP) {
return new CardImportPane.CardImportService(new Satschip(), cvc, derivation, messageProperty);
return new CardImportPane.CardImportService(new Satschip(), policyType, cvc, derivation, messageProperty);
}
return new CardImportPane.CardImportService(new Tapsigner(), cvc, derivation, messageProperty);
return new CardImportPane.CardImportService(new Tapsigner(), policyType, cvc, derivation, messageProperty);
}
@Override
@ -300,7 +301,7 @@ public class CkCardApi extends CardApi {
}
CardRead cardRead = cardProtocol.read(null, currentSlot);
Address address = getDefaultScriptType().getAddress(cardRead.getPubKey());
Address address = getDefaultScriptType().getAddress(PolicyType.SINGLE_HD, cardRead.getPubKey());
String left = addr.substring(0, addr.indexOf('_'));
String right = addr.substring(addr.lastIndexOf('_') + 1);

View File

@ -1,6 +1,7 @@
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;
@ -52,7 +53,11 @@ public class Tapsigner implements KeystoreCardImport {
}
@Override
public Keystore getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
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");
}
if(pin.length() < 6) {
throw new ImportException("PIN too short.");
}

View File

@ -249,11 +249,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);
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null, null, null);
purposeNode.setId(purposeNodeId);
}
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId(), addressNode.getAddressData());
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId(), addressNode.getAddressData(), addressNode.getSilentPaymentTweak());
addressNode.setId(nodeId);
} else if(addressNode.getAddress() != null) {
walletNodeDao.updateNodeAddressData(addressNode.getId(), addressNode.getAddressData());
@ -308,11 +308,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);
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null, null, null);
purposeNode.setId(purposeNodeId);
}
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId(), addressNode.getAddressData());
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId(), addressNode.getAddressData(), addressNode.getSilentPaymentTweak());
addressNode.setId(nodeId);
} else if(addressNode.getAddress() != null) {
walletNodeDao.updateNodeAddressData(addressNode.getId(), addressNode.getAddressData());
@ -337,6 +337,11 @@ 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);
@ -888,6 +893,13 @@ 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;
@ -906,6 +918,7 @@ 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" +
@ -927,7 +940,8 @@ 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());
"\nKeystore registrations:" + registrationKeystores.stream().map(Keystore::getDeviceRegistration).collect(Collectors.toList()) +
"\nSilent payment addresses:" + silentPaymentAddresses;
}
}
}

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.deviceRegistration, " +
@SqlQuery("select keystore.id, keystore.label, keystore.source, keystore.walletModel, keystore.masterFingerprint, keystore.derivationPath, keystore.extendedPublicKey, keystore.externalPaymentCode, keystore.silentPaymentScanAddress, 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, deviceRegistration, masterPrivateExtendedKey, seed, wallet, index) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@SqlUpdate("insert into keystore (label, source, walletModel, masterFingerprint, derivationPath, extendedPublicKey, externalPaymentCode, silentPaymentScanAddress, 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[] 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[] silentPaymentScanAddress, 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,8 +71,9 @@ 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() ? null : keystore.getExtendedPublicKey().toString(),
keystore.hasMasterPrivateKey() || wallet.isBip47() || keystore.getExtendedPublicKey() == null ? 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,6 +5,7 @@ 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;
@ -25,6 +26,7 @@ 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

@ -0,0 +1,40 @@
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

@ -0,0 +1,36 @@
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,6 +30,9 @@ public interface WalletDao {
@CreateSqlObject
DetachedLabelDao createDetachedLabelDao();
@CreateSqlObject
SilentPaymentAddressDao createSilentPaymentAddressDao();
@CreateSqlObject
WalletConfigDao createWalletConfigDao();
@ -42,21 +45,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, 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, wallet.birthHeight, 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, 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, wallet.birthHeight, 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, 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, wallet.birthHeight, 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, defaultPolicy) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@SqlUpdate("insert into wallet (name, label, network, policyType, scriptType, storedBlockHeight, gapLimit, watchLast, birthDate, birthHeight, defaultPolicy) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insert(String name, String label, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Integer watchLast, Date birthDate, long defaultPolicy);
long insert(String name, String label, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Integer watchLast, Date birthDate, Integer birthHeight, long defaultPolicy);
@SqlUpdate("update wallet set name = :name where id = :id")
void updateName(@Bind("id") long id, @Bind("name") String name);
@ -112,7 +115,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 -> walletNode.getDerivation().size() == 1).collect(Collectors.toList()));
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(WalletNode::isPurposeNode).collect(Collectors.toList()));
wallet.getPurposeNodes().forEach(walletNode -> walletNode.setWallet(wallet));
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId());
@ -121,6 +124,8 @@ 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());
@ -137,13 +142,14 @@ 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.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.getBirthHeight(), 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,6 +34,8 @@ 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, ?, " +
@SqlQuery("select walletNode.id, walletNode.derivationPath, walletNode.label, walletNode.parent, walletNode.addressData, walletNode.silentPaymentTweak, ?, " +
"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) values (?, ?, ?, ?, ?)")
@SqlUpdate("insert into walletNode (derivationPath, label, wallet, parent, addressData, silentPaymentTweak) values (?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insertWalletNode(String derivationPath, String label, long wallet, Long parent, byte[] addressData);
long insertWalletNode(String derivationPath, String label, long wallet, Long parent, byte[] addressData, byte[] silentPaymentTweak);
@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);
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), truncate(purposeNode.getLabel()), wallet.getId(), null, 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());
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), truncate(addressNode.getLabel()), wallet.getId(), purposeNodeId, addressNode.getAddressData(), addressNode.getSilentPaymentTweak());
addressNode.setId(addressNodeId);
addTransactionOutputs(addressNode);
}

View File

@ -16,9 +16,10 @@ public class WalletNodeMapper implements RowMapper<WalletNode> {
walletNode.setLabel(rs.getString("walletNode.label"));
byte[] addressData = rs.getBytes("walletNode.addressData");
if(addressData != null) {
ScriptType scriptType = ScriptType.values()[rs.getInt(6)];
ScriptType scriptType = ScriptType.values()[rs.getInt(7)];
walletNode.setAddress(scriptType.getAddress(addressData));
}
walletNode.setSilentPaymentTweak(rs.getBytes("walletNode.silentPaymentTweak"));
return walletNode;
}
}

View File

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.io.keycard;
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.ImportException;
@ -52,7 +53,11 @@ public class Keycard implements KeystoreCardImport {
}
@Override
public Keystore getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
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");
}
if(!StringUtils.isNumeric(pin)) {
throw new ImportException("PIN must be all digits.");
}

View File

@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECDSASignature;
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;
@ -145,8 +146,8 @@ public class KeycardApi extends CardApi {
}
@Override
public Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty) {
return new CardImportPane.CardImportService(new Keycard(), pin, derivation, messageProperty);
public Service<Keystore> getImportService(PolicyType policyType, List<ChildNumber> derivation, StringProperty messageProperty) {
return new CardImportPane.CardImportService(new Keycard(), policyType, pin, derivation, messageProperty);
}
private byte[] compressedPub(byte[] uncompressedPub) {

View File

@ -9,6 +9,7 @@ import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.crypto.ECDSASignature;
import com.sparrowwallet.drongo.crypto.SchnorrSignature;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
@ -122,8 +123,8 @@ public class SatoCardApi extends CardApi {
}
@Override
public Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty) {
return new CardImportPane.CardImportService(new Satochip(), pin, derivation, messageProperty);
public Service<Keystore> getImportService(PolicyType policyType, List<ChildNumber> derivation, StringProperty messageProperty) {
return new CardImportPane.CardImportService(new Satochip(), policyType, pin, derivation, messageProperty);
}
/*

View File

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.io.satochip;
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;
@ -51,7 +52,11 @@ public class Satochip implements KeystoreCardImport {
}
@Override
public Keystore getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
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");
}
if(pin.length() < 4) {
throw new ImportException("PIN too short.");
}

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