mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-01 12:36:14 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c3a00b71b | ||
|
|
ced4d4d337 | ||
|
|
4a3ad9f4ff | ||
|
|
f5a72105ac | ||
|
|
0502eec0cd | ||
|
|
8e6933b5ca | ||
|
|
1fd1dec6cf | ||
|
|
422713ff53 | ||
|
|
ada45ee75b | ||
|
|
2f153686dd | ||
|
|
f691f1691e | ||
|
|
1f9e37b40c | ||
|
|
a1d2de1859 | ||
|
|
143472bdfc | ||
|
|
b9e64d42ff | ||
|
|
9a09bb8cda | ||
|
|
4b028af123 | ||
|
|
8033e5fd88 | ||
|
|
badf8c8f2f | ||
|
|
b6a353815c | ||
|
|
ea2f858dc9 | ||
|
|
324540009a | ||
|
|
1c1f90344f | ||
|
|
094dd45547 | ||
|
|
6d434722cc | ||
|
|
c8a4ed0c3d |
@@ -12,16 +12,11 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 15.0.2
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: joschi/setup-jdk@v2
|
||||
- name: Set up JDK 16.0.1
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 15
|
||||
- name: Set up JDK 14.0.2
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 14.0.2
|
||||
distribution: 'adopt'
|
||||
java-version: '16.0.1'
|
||||
- name: Show Build Versions
|
||||
run: ./gradlew -v
|
||||
- name: Cache Gradle packages
|
||||
|
||||
+225
-18
@@ -1,11 +1,11 @@
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.9'
|
||||
id 'org.kordamp.gradle.jdeps' version '0.9.0'
|
||||
id 'org.beryx.jlink' version '2.22.0'
|
||||
id 'extra-java-module-info'
|
||||
id 'com.dua3.javafxgradle7plugin' version '0.0.9'
|
||||
id 'org.beryx.jlink' version '2.24.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.4.2'
|
||||
def sparrowVersion = '1.4.3'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -29,7 +29,7 @@ tasks.withType(AbstractArchiveTask) {
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = "15"
|
||||
version = "16"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
@@ -53,19 +53,28 @@ dependencies {
|
||||
implementation('org.flywaydb:flyway-core:7.10.5-SNAPSHOT')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||
implementation('com.google.zxing:javase:3.4.0')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.0')
|
||||
implementation('com.google.zxing:javase:3.4.0') {
|
||||
exclude group: 'com.beust', module: 'jcommander'
|
||||
}
|
||||
implementation('com.beust:jcommander:1.81')
|
||||
implementation('com.github.arteam:simple-json-rpc-core:1.0')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.0') {
|
||||
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
||||
}
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.0')
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.1')
|
||||
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
}
|
||||
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
|
||||
exclude group: 'com.nativelibs4java', module: 'bridj'
|
||||
}
|
||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8")
|
||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8") {
|
||||
exclude group: 'org.jetbrains.kotlin'
|
||||
}
|
||||
implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20')
|
||||
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
@@ -76,7 +85,7 @@ dependencies {
|
||||
exclude group: 'org.openjfx', module: 'javafx-web'
|
||||
exclude group: 'org.openjfx', module: 'javafx-media'
|
||||
}
|
||||
implementation('dev.bwt:bwt-jni:0.1.7')
|
||||
implementation('dev.bwt:bwt-jni:0.1.8')
|
||||
implementation('net.sourceforge.javacsv:javacsv:2.0')
|
||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||
exclude group: 'org.slf4j'
|
||||
@@ -84,7 +93,10 @@ dependencies {
|
||||
testImplementation('junit:junit:4.12')
|
||||
}
|
||||
|
||||
mainClassName = 'com.sparrowwallet.sparrow/com.sparrowwallet.sparrow.MainApp'
|
||||
application {
|
||||
mainModule = 'com.sparrowwallet.sparrow'
|
||||
mainClass = 'com.sparrowwallet.sparrow.MainApp'
|
||||
}
|
||||
|
||||
compileJava {
|
||||
options.with {
|
||||
@@ -135,9 +147,9 @@ jlink {
|
||||
requires 'java.xml'
|
||||
requires 'java.logging'
|
||||
requires 'javafx.base'
|
||||
requires 'com.fasterxml.jackson.databind'
|
||||
requires 'jdk.crypto.cryptoki'
|
||||
requires 'java.management'
|
||||
requires 'io.leangen.geantyref'
|
||||
uses 'org.flywaydb.core.extensibility.FlywayExtension'
|
||||
uses 'org.flywaydb.core.internal.database.DatabaseType'
|
||||
}
|
||||
@@ -154,17 +166,18 @@ jlink {
|
||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.sql",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow"]
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
|
||||
"--add-reads=com.sparrowwallet.merged.module=logback.classic"]
|
||||
|
||||
if(os.macOsX) {
|
||||
jvmArgs += "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"
|
||||
@@ -216,3 +229,197 @@ task packageTarDistribution(type: Tar) {
|
||||
include "Sparrow/**"
|
||||
}
|
||||
}
|
||||
|
||||
extraJavaModuleInfo {
|
||||
module('tornadofx-controls-1.0.4.jar', 'tornadofx.controls', '1.0.4') {
|
||||
exports('tornadofx.control')
|
||||
requires('javafx.controls')
|
||||
}
|
||||
module('simple-json-rpc-core-1.0.jar', 'simple.json.rpc.core', '1.0') {
|
||||
exports('com.github.arteam.simplejsonrpc.core.annotation')
|
||||
exports('com.github.arteam.simplejsonrpc.core.domain')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
requires('com.fasterxml.jackson.annotation')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('org.jetbrains.annotations')
|
||||
}
|
||||
module('simple-json-rpc-client-1.0.jar', 'simple.json.rpc.client', '1.0') {
|
||||
exports('com.github.arteam.simplejsonrpc.client')
|
||||
exports('com.github.arteam.simplejsonrpc.client.builder')
|
||||
exports('com.github.arteam.simplejsonrpc.client.exception')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('simple.json.rpc.core')
|
||||
}
|
||||
module('simple-json-rpc-server-1.0.jar', 'simple.json.rpc.server', '1.0') {
|
||||
exports('com.github.arteam.simplejsonrpc.server')
|
||||
requires('simple.json.rpc.core')
|
||||
requires('com.google.common')
|
||||
requires('org.slf4j')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
}
|
||||
module('bridj-0.7-20140918-3.jar', 'com.nativelibs4java.bridj', '0.7-20140918-3') {
|
||||
exports('org.bridj')
|
||||
exports('org.bridj.cpp')
|
||||
requires('java.logging')
|
||||
}
|
||||
module('webcam-capture-0.3.13-SNAPSHOT.jar', 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
|
||||
exports('com.github.sarxos.webcam')
|
||||
exports('com.github.sarxos.webcam.ds.buildin')
|
||||
exports('com.github.sarxos.webcam.ds.buildin.natives')
|
||||
requires('java.desktop')
|
||||
requires('com.nativelibs4java.bridj')
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('centerdevice-nsmenufx-2.1.7.jar', 'centerdevice.nsmenufx', '2.1.7') {
|
||||
exports('de.codecentric.centerdevice')
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
|
||||
exports('com.csvreader')
|
||||
}
|
||||
module('jul-to-slf4j-1.7.30.jar', 'org.slf4j.jul.to.slf4j', '1.7.30') {
|
||||
exports('org.slf4j.bridge')
|
||||
requires('java.logging')
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
|
||||
exports('org.zeromq')
|
||||
}
|
||||
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
|
||||
exports('org.json.simple')
|
||||
}
|
||||
module('logback-classic-1.2.3.jar', 'logback.classic', '1.2.3') {
|
||||
exports('ch.qos.logback.classic')
|
||||
requires('org.slf4j')
|
||||
requires('logback.core')
|
||||
requires('java.xml')
|
||||
requires('java.logging')
|
||||
}
|
||||
module('kotlin-logging-1.5.4.jar', 'io.github.microutils.kotlin.logging', '1.5.4') {
|
||||
exports('mu')
|
||||
requires('kotlin.stdlib')
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('failureaccess-1.0.1.jar', 'failureaccess', '1.0.1') {
|
||||
exports('com.google.common.util.concurrent.internal')
|
||||
}
|
||||
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
|
||||
module('guava-28.2-jre.jar', 'com.google.common', '28.2-jre') {
|
||||
exports('com.google.common.eventbus')
|
||||
exports('com.google.common.net')
|
||||
exports('com.google.common.base')
|
||||
exports('com.google.common.collect')
|
||||
exports('com.google.common.io')
|
||||
requires('failureaccess')
|
||||
requires('java.logging')
|
||||
}
|
||||
module('jsr305-3.0.2.jar', 'com.google.code.findbugs.jsr305', '3.0.2')
|
||||
module('j2objc-annotations-1.3.jar', 'com.google.j2objc.j2objc.annotations', '1.3')
|
||||
module('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
|
||||
exports('org.jdbi.v3.core')
|
||||
exports('org.jdbi.v3.core.mapper')
|
||||
exports('org.jdbi.v3.core.statement')
|
||||
exports('org.jdbi.v3.core.result')
|
||||
exports('org.jdbi.v3.core.h2')
|
||||
exports('org.jdbi.v3.core.spi')
|
||||
requires('io.leangen.geantyref')
|
||||
requires('java.sql')
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
|
||||
exports('io.leangen.geantyref')
|
||||
}
|
||||
module('richtextfx-0.10.4.jar', 'org.fxmisc.richtext', '0.10.4') {
|
||||
exports('org.fxmisc.richtext')
|
||||
exports('org.fxmisc.richtext.event')
|
||||
exports('org.fxmisc.richtext.model')
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
requires('org.fxmisc.flowless')
|
||||
requires('org.reactfx.reactfx')
|
||||
requires('org.fxmisc.undo.undofx')
|
||||
requires('org.fxmisc.wellbehaved')
|
||||
}
|
||||
module('undofx-2.1.0.jar', 'org.fxmisc.undo.undofx', '2.1.0') {
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
requires('org.reactfx.reactfx')
|
||||
}
|
||||
module('flowless-0.6.1.jar', 'org.fxmisc.flowless', '0.6.1') {
|
||||
exports('org.fxmisc.flowless')
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
requires('org.reactfx.reactfx')
|
||||
}
|
||||
module('reactfx-2.0-M5.jar', 'org.reactfx.reactfx', '2.0-M5') {
|
||||
exports('org.reactfx')
|
||||
exports('org.reactfx.value')
|
||||
exports('org.reactfx.collection')
|
||||
exports('org.reactfx.util')
|
||||
requires('javafx.base')
|
||||
requires('javafx.graphics')
|
||||
requires('javafx.controls')
|
||||
}
|
||||
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
|
||||
requires('javafx.base')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
|
||||
module('kotlin-stdlib-jdk8-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk8', '1.5.20')
|
||||
module('kotlin-stdlib-jdk7-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk7', '1.5.20')
|
||||
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
|
||||
exports('kotlin')
|
||||
}
|
||||
module('hummingbird-1.6.1.jar', 'com.sparrowwallet.hummingbird', '1.6.1') {
|
||||
exports('com.sparrowwallet.hummingbird')
|
||||
exports('com.sparrowwallet.hummingbird.registry')
|
||||
requires('co.nstant.in.cbor')
|
||||
}
|
||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||
exports('co.nstant.in.cbor')
|
||||
}
|
||||
module("netlayer-jpms-${osName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
|
||||
exports('org.berndpruenster.netlayer.tor')
|
||||
requires('com.github.ravn.jsocks')
|
||||
requires('com.github.JesusMcCloud.jtorctl')
|
||||
requires('kotlin.stdlib')
|
||||
requires('commons.compress')
|
||||
requires('org.tukaani.xz')
|
||||
requires('java.management')
|
||||
requires('io.github.microutils.kotlin.logging')
|
||||
}
|
||||
module('jtorctl-1.5.jar', 'com.github.JesusMcCloud.jtorctl', '1.5') {
|
||||
exports('net.freehaven.tor.control')
|
||||
}
|
||||
module('commons-compress-1.18.jar', 'commons.compress', '1.18') {
|
||||
exports('org.apache.commons.compress')
|
||||
requires('org.tukaani.xz')
|
||||
}
|
||||
module('xz-1.6.jar', 'org.tukaani.xz', '1.6') {
|
||||
exports('org.tukaani.xz')
|
||||
}
|
||||
module('jsocks-1.0.jar', 'com.github.ravn.jsocks', '1.0') {
|
||||
exports('com.runjva.sourceforge.jsocks.protocol')
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
|
||||
module('logback-core-1.2.3.jar', 'logback.core', '1.2.3') {
|
||||
requires('java.xml')
|
||||
}
|
||||
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
|
||||
exports('kotlin.jvm')
|
||||
exports('kotlin.collections')
|
||||
}
|
||||
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
|
||||
exports('com.beust.jcommander')
|
||||
}
|
||||
module('junit-4.12.jar', 'junit', '4.12') {
|
||||
exports('org.junit')
|
||||
}
|
||||
module('hamcrest-core-1.3.jar', 'org.hamcrest.core', '1.3')
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
plugins {
|
||||
id 'java-gradle-plugin' // so we can assign and ID to our plugin
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.ow2.asm:asm:8.0.1'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
// here we register our plugin with an ID
|
||||
register("extra-java-module-info") {
|
||||
id = "extra-java-module-info"
|
||||
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.attributes.Attribute;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
|
||||
/**
|
||||
* Entry point of our plugin that should be applied in the root project.
|
||||
*/
|
||||
public class ExtraModuleInfoPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
|
||||
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
|
||||
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
|
||||
|
||||
// setup the transform for all projects in the build
|
||||
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
|
||||
}
|
||||
|
||||
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
|
||||
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
|
||||
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
|
||||
|
||||
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
|
||||
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
|
||||
c -> c.getAttributes().attribute(javaModule, true));
|
||||
|
||||
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
|
||||
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
|
||||
|
||||
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
|
||||
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
|
||||
t.parameters(p -> {
|
||||
p.setModuleInfo(extension.getModuleInfo());
|
||||
p.setAutomaticModules(extension.getAutomaticModules());
|
||||
});
|
||||
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
|
||||
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
|
||||
if (!configuration.isCanBeResolved()) {
|
||||
return false;
|
||||
}
|
||||
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
||||
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
||||
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
|
||||
import org.gradle.api.Action;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A data class to collect all the module information we want to add.
|
||||
* Here the class is used as extension that can be configured in the build script
|
||||
* and as input to the ExtraModuleInfoTransform that add the information to Jars.
|
||||
*/
|
||||
public class ExtraModuleInfoPluginExtension {
|
||||
|
||||
private final Map<String, ModuleInfo> moduleInfo = new HashMap<>();
|
||||
private final Map<String, String> automaticModules = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Add full module information for a given Jar file.
|
||||
*/
|
||||
public void module(String jarName, String moduleName, String moduleVersion) {
|
||||
module(jarName, moduleName, moduleVersion, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add full module information, including exported packages and dependencies, for a given Jar file.
|
||||
*/
|
||||
public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action<? super ModuleInfo> conf) {
|
||||
ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion);
|
||||
if (conf != null) {
|
||||
conf.execute(moduleInfo);
|
||||
}
|
||||
this.moduleInfo.put(jarName, moduleInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add only an automatic module name to a given jar file.
|
||||
*/
|
||||
public void automaticModule(String jarName, String moduleName) {
|
||||
automaticModules.put(jarName, moduleName);
|
||||
}
|
||||
|
||||
protected Map<String, ModuleInfo> getModuleInfo() {
|
||||
return moduleInfo;
|
||||
}
|
||||
|
||||
protected Map<String, String> getAutomaticModules() {
|
||||
return automaticModules;
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import org.gradle.api.artifacts.transform.InputArtifact;
|
||||
import org.gradle.api.artifacts.transform.TransformAction;
|
||||
import org.gradle.api.artifacts.transform.TransformOutputs;
|
||||
import org.gradle.api.artifacts.transform.TransformParameters;
|
||||
import org.gradle.api.file.FileSystemLocation;
|
||||
import org.gradle.api.provider.Provider;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.ModuleVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.jar.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
/**
|
||||
* An artifact transform that applies additional information to Jars without module information.
|
||||
* The transformation fails the build if a Jar does not contain information and no extra information
|
||||
* was defined for it. This way we make sure that all Jars are turned into modules.
|
||||
*/
|
||||
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
|
||||
|
||||
public static class Parameter implements TransformParameters, Serializable {
|
||||
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
|
||||
private Map<String, String> automaticModules = Collections.emptyMap();
|
||||
|
||||
@Input
|
||||
public Map<String, ModuleInfo> getModuleInfo() {
|
||||
return moduleInfo;
|
||||
}
|
||||
|
||||
@Input
|
||||
public Map<String, String> getAutomaticModules() {
|
||||
return automaticModules;
|
||||
}
|
||||
|
||||
public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
|
||||
this.moduleInfo = moduleInfo;
|
||||
}
|
||||
|
||||
public void setAutomaticModules(Map<String, String> automaticModules) {
|
||||
this.automaticModules = automaticModules;
|
||||
}
|
||||
}
|
||||
|
||||
@InputArtifact
|
||||
protected abstract Provider<FileSystemLocation> getInputArtifact();
|
||||
|
||||
@Override
|
||||
public void transform(TransformOutputs outputs) {
|
||||
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
|
||||
Map<String, String> automaticModules = getParameters().automaticModules;
|
||||
File originalJar = getInputArtifact().get().getAsFile();
|
||||
String originalJarName = originalJar.getName();
|
||||
|
||||
if (isModule(originalJar)) {
|
||||
outputs.file(originalJar);
|
||||
} else if (moduleInfo.containsKey(originalJarName)) {
|
||||
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
|
||||
} else if (isAutoModule(originalJar)) {
|
||||
outputs.file(originalJar);
|
||||
} else if (automaticModules.containsKey(originalJarName)) {
|
||||
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
|
||||
} else {
|
||||
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isModule(File jar) {
|
||||
Pattern moduleInfoClassMrjarPath = Pattern.compile("META-INF/versions/\\d+/module-info.class");
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
|
||||
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
|
||||
ZipEntry next = inputStream.getNextEntry();
|
||||
while (next != null) {
|
||||
if ("module-info.class".equals(next.getName())) {
|
||||
return true;
|
||||
}
|
||||
if (isMultiReleaseJar && moduleInfoClassMrjarPath.matcher(next.getName()).matches()) {
|
||||
return true;
|
||||
}
|
||||
next = inputStream.getNextEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean containsMultiReleaseJarEntry(JarInputStream jarStream) {
|
||||
Manifest manifest = jarStream.getManifest();
|
||||
return manifest != null && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"));
|
||||
}
|
||||
|
||||
private boolean isAutoModule(File jar) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
|
||||
return inputStream.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private File getModuleJar(TransformOutputs outputs, File originalJar) {
|
||||
return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-module.jar");
|
||||
}
|
||||
|
||||
private static void addAutomaticModuleName(File originalJar, File moduleJar, String moduleName) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
|
||||
Manifest manifest = inputStream.getManifest();
|
||||
manifest.getMainAttributes().put(new Attributes.Name("Automatic-Module-Name"), moduleName);
|
||||
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
|
||||
copyEntries(inputStream, outputStream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
|
||||
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
|
||||
copyEntries(inputStream, outputStream);
|
||||
outputStream.putNextEntry(new JarEntry("module-info.class"));
|
||||
outputStream.write(addModuleInfo(moduleInfo));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
|
||||
JarEntry jarEntry = inputStream.getNextJarEntry();
|
||||
while (jarEntry != null) {
|
||||
outputStream.putNextEntry(jarEntry);
|
||||
outputStream.write(inputStream.readAllBytes());
|
||||
outputStream.closeEntry();
|
||||
jarEntry = inputStream.getNextJarEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] addModuleInfo(ModuleInfo moduleInfo) {
|
||||
ClassWriter classWriter = new ClassWriter(0);
|
||||
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
|
||||
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.getModuleVersion());
|
||||
for (String packageName : moduleInfo.getExports()) {
|
||||
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
|
||||
}
|
||||
moduleVisitor.visitRequire("java.base", 0, null);
|
||||
for (String requireName : moduleInfo.getRequires()) {
|
||||
moduleVisitor.visitRequire(requireName, 0, null);
|
||||
}
|
||||
for (String requireName : moduleInfo.getRequiresTransitive()) {
|
||||
moduleVisitor.visitRequire(requireName, Opcodes.ACC_TRANSITIVE, null);
|
||||
}
|
||||
moduleVisitor.visitEnd();
|
||||
classWriter.visitEnd();
|
||||
return classWriter.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data class to hold the information that should be added as module-info.class to an existing Jar file.
|
||||
*/
|
||||
public class ModuleInfo implements Serializable {
|
||||
private String moduleName;
|
||||
private String moduleVersion;
|
||||
private List<String> exports = new ArrayList<>();
|
||||
private List<String> requires = new ArrayList<>();
|
||||
private List<String> requiresTransitive = new ArrayList<>();
|
||||
|
||||
ModuleInfo(String moduleName, String moduleVersion) {
|
||||
this.moduleName = moduleName;
|
||||
this.moduleVersion = moduleVersion;
|
||||
}
|
||||
|
||||
public void exports(String exports) {
|
||||
this.exports.add(exports);
|
||||
}
|
||||
|
||||
public void requires(String requires) {
|
||||
this.requires.add(requires);
|
||||
}
|
||||
|
||||
public void requiresTransitive(String requiresTransitive) {
|
||||
this.requiresTransitive.add(requiresTransitive);
|
||||
}
|
||||
|
||||
public String getModuleName() {
|
||||
return moduleName;
|
||||
}
|
||||
|
||||
protected String getModuleVersion() {
|
||||
return moduleVersion;
|
||||
}
|
||||
|
||||
protected List<String> getExports() {
|
||||
return exports;
|
||||
}
|
||||
|
||||
protected List<String> getRequires() {
|
||||
return requires;
|
||||
}
|
||||
|
||||
protected List<String> getRequiresTransitive() {
|
||||
return requiresTransitive;
|
||||
}
|
||||
}
|
||||
+1
-1
Submodule drongo updated: c021300797...db081695e8
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -82,6 +82,7 @@ esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
@@ -129,6 +130,7 @@ fi
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
|
||||
Vendored
+4
-18
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -54,7 +54,7 @@ goto fail
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
@@ -64,28 +64,14 @@ echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.2</string>
|
||||
<string>1.4.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -3,10 +3,7 @@ package com.sparrowwallet.sparrow;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.io.ByteSource;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
@@ -801,7 +798,10 @@ public class AppController implements Initializable {
|
||||
} catch(StorageException e) {
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||
} catch(Exception e) {
|
||||
if(!attemptImportWallet(file, null)) {
|
||||
if(e instanceof IOException && e.getMessage().startsWith("The process cannot access the file because another process has locked")) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", "The wallet file is locked. Is another instance of " + MainApp.APP_NAME + " already running?");
|
||||
} else if(!attemptImportWallet(file, null)) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage() == null ? "Unsupported file format" : e.getMessage());
|
||||
}
|
||||
@@ -897,7 +897,8 @@ public class AppController implements Initializable {
|
||||
new SpecterDesktop(),
|
||||
new CoboVaultSinglesig(), new CoboVaultMultisig(),
|
||||
new PassportSinglesig(),
|
||||
new KeystoneSinglesig(), new KeystoneMultisig());
|
||||
new KeystoneSinglesig(), new KeystoneMultisig(),
|
||||
new CaravanMultisig());
|
||||
for(WalletImport importer : walletImporters) {
|
||||
try(FileInputStream inputStream = new FileInputStream(file)) {
|
||||
if(importer.isEncrypted(file) && password == null) {
|
||||
|
||||
@@ -31,7 +31,7 @@ import java.util.stream.Collectors;
|
||||
public class MainApp extends Application {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.4.2";
|
||||
public static final String APP_VERSION = "1.4.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";
|
||||
|
||||
@@ -546,7 +546,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
|
||||
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType scriptType = Arrays.stream(ScriptType.SINGLE_HASH_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().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
|
||||
wallet.setName(device.getModel().toDisplayString());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
@@ -118,8 +119,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
});
|
||||
actionBox.getChildren().add(receiveButton);
|
||||
|
||||
if(nodeEntry.getWallet().getKeystores().size() == 1 &&
|
||||
(nodeEntry.getWallet().getKeystores().get(0).hasPrivateKey() || nodeEntry.getWallet().getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
|
||||
if(canSignMessage(nodeEntry.getWallet())) {
|
||||
Button signMessageButton = new Button("");
|
||||
signMessageButton.setGraphic(getSignMessageGlyph());
|
||||
signMessageButton.setOnAction(event -> {
|
||||
@@ -127,7 +127,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
messageSignDialog.showAndWait();
|
||||
});
|
||||
actionBox.getChildren().add(signMessageButton);
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
@@ -268,6 +267,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
|
||||
}
|
||||
|
||||
private static boolean canSignMessage(Wallet wallet) {
|
||||
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
|
||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB);
|
||||
}
|
||||
|
||||
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||
return transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
@@ -292,14 +296,28 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(hashIndexEntry.getWallet(), spendingUtxos)));
|
||||
}
|
||||
|
||||
private static void freezeUtxo(HashIndexEntry hashIndexEntry) {
|
||||
hashIndexEntry.getHashIndex().setStatus(Status.FROZEN);
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
|
||||
private static void freezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
|
||||
.map(tp -> tp.getTreeItem().getValue())
|
||||
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||
.map(e -> ((HashIndexEntry)e).getHashIndex())
|
||||
.filter(ref -> ref.getStatus() != Status.FROZEN)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
utxos.forEach(ref -> ref.setStatus(Status.FROZEN));
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
|
||||
}
|
||||
|
||||
private static void unfreezeUtxo(HashIndexEntry hashIndexEntry) {
|
||||
hashIndexEntry.getHashIndex().setStatus(null);
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
|
||||
private static void unfreezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
|
||||
.map(tp -> tp.getTreeItem().getValue())
|
||||
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||
.map(e -> ((HashIndexEntry)e).getHashIndex())
|
||||
.filter(ref -> ref.getStatus() == Status.FROZEN)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
utxos.forEach(ref -> ref.setStatus(null));
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
|
||||
}
|
||||
|
||||
private static Glyph getViewTransactionGlyph() {
|
||||
@@ -449,7 +467,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
});
|
||||
getItems().add(receiveToAddress);
|
||||
|
||||
if(nodeEntry != null) {
|
||||
if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) {
|
||||
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
||||
signVerifyMessage.setGraphic(getSignMessageGlyph());
|
||||
signVerifyMessage.setOnAction(AE -> {
|
||||
@@ -514,7 +532,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
freezeUtxo.setGraphic(getFreezeGlyph());
|
||||
freezeUtxo.setOnAction(AE -> {
|
||||
hide();
|
||||
freezeUtxo(hashIndexEntry);
|
||||
freezeUtxo(treeTableView, hashIndexEntry);
|
||||
});
|
||||
getItems().add(freezeUtxo);
|
||||
} else {
|
||||
@@ -522,7 +540,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
unfreezeUtxo.setGraphic(getUnfreezeGlyph());
|
||||
unfreezeUtxo.setOnAction(AE -> {
|
||||
hide();
|
||||
unfreezeUtxo(hashIndexEntry);
|
||||
unfreezeUtxo(treeTableView, hashIndexEntry);
|
||||
});
|
||||
getItems().add(unfreezeUtxo);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
|
||||
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
|
||||
public static final int MAX_PERIOD_HOURS = 2;
|
||||
private static final double Y_VALUE_BREAK_MVB = 3.0;
|
||||
|
||||
private Tooltip tooltip;
|
||||
|
||||
@@ -68,19 +69,6 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
categoryAxis.setTickLabelRotation(0);
|
||||
|
||||
NumberAxis numberAxis = (NumberAxis)getYAxis();
|
||||
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
long vSizeBytes = object.longValue();
|
||||
return (vSizeBytes / (1000 * 1000)) + " MvB";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
this.setOnMouseMoved(mouseEvent -> {
|
||||
Point2D sceneCoords = this.localToScene(mouseEvent.getX(), mouseEvent.getY());
|
||||
String category = categoryAxis.getValueForDisplay(categoryAxis.sceneToLocal(sceneCoords).getX());
|
||||
@@ -116,6 +104,24 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
previousFeeRate = feeRate;
|
||||
}
|
||||
|
||||
final double maxMvB = getMaxMvB(getData());
|
||||
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
long vSizeBytes = object.longValue();
|
||||
if(maxMvB > Y_VALUE_BREAK_MVB) {
|
||||
return (vSizeBytes / (1000 * 1000)) + " MvB";
|
||||
} else {
|
||||
return (vSizeBytes / (1000)) + " kvB";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if(categories.keySet().iterator().hasNext()) {
|
||||
String time = categories.values().iterator().next();
|
||||
tooltip.setGraphic(new ChartTooltip(time, time, getData()));
|
||||
@@ -159,20 +165,42 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
return categories;
|
||||
}
|
||||
|
||||
private static double getMaxMvB(List<Series<String, Number>> seriesList) {
|
||||
double maxTotal = 0d;
|
||||
for(Series<String, Number> series : seriesList) {
|
||||
maxTotal = Math.max(maxTotal, getMaxMvB(series));
|
||||
}
|
||||
|
||||
return maxTotal;
|
||||
}
|
||||
|
||||
private static double getMaxMvB(Series<String, Number> series) {
|
||||
double total = 0d;
|
||||
for(XYChart.Data<String, Number> data : series.getData()) {
|
||||
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
|
||||
total += mvb;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static class ChartTooltip extends VBox {
|
||||
public ChartTooltip(String category, String time, List<Series<String, Number>> seriesList) {
|
||||
Label title = new Label("At " + time);
|
||||
HBox titleBox = new HBox(title);
|
||||
title.setStyle("-fx-alignment: center; -fx-font-size: 12px; -fx-padding: 0 0 5 0;");
|
||||
getChildren().add(titleBox);
|
||||
double maxMvB = getMaxMvB(seriesList);
|
||||
|
||||
for(int i = seriesList.size() - 1; i >= 0; i--) {
|
||||
Series<String, Number> series = seriesList.get(i);
|
||||
for(XYChart.Data<String, Number> data : series.getData()) {
|
||||
if(data.getXValue().equals(category)) {
|
||||
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
|
||||
if(mvb >= 0.01) {
|
||||
Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB");
|
||||
double kvb = data.getYValue().doubleValue() / 1000;
|
||||
double mvb = kvb / 1000;
|
||||
if(mvb >= 0.01 || (maxMvB < Y_VALUE_BREAK_MVB && mvb > 0.001)) {
|
||||
String amount = (maxMvB < Y_VALUE_BREAK_MVB ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
|
||||
Label label = new Label(series.getName() + ": " + amount);
|
||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
||||
if(i < 8) {
|
||||
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
|
||||
|
||||
@@ -51,6 +51,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private final Wallet wallet;
|
||||
private WalletNode walletNode;
|
||||
private boolean electrumSignatureFormat;
|
||||
private boolean closed;
|
||||
|
||||
/**
|
||||
* Verification only constructor
|
||||
@@ -177,7 +178,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
|
||||
ButtonType verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
|
||||
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
|
||||
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
if(buttons.length > 0) {
|
||||
dialogPane.getButtonTypes().addAll(buttons);
|
||||
@@ -189,7 +190,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
customSignButton.setOnAction(event -> {
|
||||
customSignButton.setDisable(true);
|
||||
signMessage();
|
||||
setResult(ButtonBar.ButtonData.OK_DONE);
|
||||
setResult(ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -240,9 +241,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
EventManager.get().unregister(this);
|
||||
if(!closed) {
|
||||
EventManager.get().unregister(this);
|
||||
closed = true;
|
||||
}
|
||||
});
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||
|
||||
@@ -277,8 +282,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
private boolean isValidAddress() {
|
||||
try {
|
||||
getAddress();
|
||||
return true;
|
||||
Address address = getAddress();
|
||||
return address.getScriptType() != ScriptType.P2TR;
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -311,7 +316,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||
String signatureText = privKey.signMessage(message.getText().trim(), scriptType, null);
|
||||
String signatureText = privKey.signMessage(message.getText().trim(), scriptType);
|
||||
signature.clear();
|
||||
signature.appendText(signatureText);
|
||||
privKey.clear();
|
||||
|
||||
@@ -20,6 +20,7 @@ import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Callback;
|
||||
import org.controlsfx.control.textfield.AutoCompletionBinding;
|
||||
@@ -163,7 +164,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNext(wordEntries.get(i + 1).getEditor());
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
|
||||
}
|
||||
wordsPane.getChildren().addAll(wordEntries);
|
||||
|
||||
@@ -171,7 +173,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
|
||||
if(!displayWordsOnly) {
|
||||
PassphraseEntry passphraseEntry = new PassphraseEntry();
|
||||
wordEntries.get(wordEntries.size() - 1).setNext(passphraseEntry.getEditor());
|
||||
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
|
||||
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
|
||||
vBox.getChildren().add(passphraseEntry);
|
||||
|
||||
@@ -401,7 +403,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
private static class WordEntry extends HBox {
|
||||
private static List<String> wordList;
|
||||
private final TextField wordField;
|
||||
private Node next;
|
||||
private WordEntry nextEntry;
|
||||
private TextField nextField;
|
||||
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
|
||||
super();
|
||||
@@ -411,7 +414,29 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
Label label = new Label((wordNumber+1) + ".");
|
||||
label.setPrefWidth(22);
|
||||
label.setAlignment(Pos.CENTER_RIGHT);
|
||||
wordField = new TextField();
|
||||
wordField = new TextField() {
|
||||
@Override
|
||||
public void paste() {
|
||||
Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||
if(clipboard.hasString() && clipboard.getString().matches("(?m).+[\\n\\s][\\S\\s]*")) {
|
||||
String[] words = clipboard.getString().split("[\\n\\s]");
|
||||
WordEntry entry = WordEntry.this;
|
||||
for(String word : words) {
|
||||
if(entry.nextField != null) {
|
||||
entry.nextField.requestFocus();
|
||||
}
|
||||
|
||||
entry.wordField.setText(word);
|
||||
entry = entry.nextEntry;
|
||||
if(entry == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.paste();
|
||||
}
|
||||
}
|
||||
};
|
||||
wordField.setMaxWidth(100);
|
||||
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
|
||||
String text = change.getText();
|
||||
@@ -431,8 +456,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
|
||||
autoCompletionBinding.setDelay(50);
|
||||
autoCompletionBinding.setOnAutoCompleted(event -> {
|
||||
if (next != null) {
|
||||
next.requestFocus();
|
||||
if(nextField != null) {
|
||||
nextField.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -454,8 +479,12 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
return wordField;
|
||||
}
|
||||
|
||||
public void setNext(Node node) {
|
||||
next = node;
|
||||
public void setNextEntry(WordEntry nextEntry) {
|
||||
this.nextEntry = nextEntry;
|
||||
}
|
||||
|
||||
public void setNextField(TextField field) {
|
||||
this.nextField = field;
|
||||
}
|
||||
|
||||
public static boolean isValid(String word) {
|
||||
|
||||
@@ -33,9 +33,9 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class);
|
||||
|
||||
private static final int MIN_FRAGMENT_LENGTH = 10;
|
||||
private static final int MAX_FRAGMENT_LENGTH = 100;
|
||||
private static final int MAX_FRAGMENT_LENGTH = 250;
|
||||
|
||||
private static final int ANIMATION_PERIOD_MILLIS = 400;
|
||||
private static final int ANIMATION_PERIOD_MILLIS = 200;
|
||||
|
||||
private static final int QR_WIDTH = 480;
|
||||
private static final int QR_HEIGHT = 480;
|
||||
@@ -232,7 +232,7 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
if(useLegacyEncoding) {
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
|
||||
} else {
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.github.sarxos.webcam.WebcamEvent;
|
||||
import com.github.sarxos.webcam.WebcamListener;
|
||||
import com.github.sarxos.webcam.WebcamResolution;
|
||||
import com.github.sarxos.webcam.WebcamUpdater;
|
||||
import com.github.sarxos.webcam.*;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
@@ -41,6 +38,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Borders;
|
||||
import org.slf4j.Logger;
|
||||
@@ -74,6 +72,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
|
||||
|
||||
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
||||
|
||||
public QRScanDialog() {
|
||||
this.decoder = new URDecoder();
|
||||
this.legacyDecoder = new LegacyURDecoder();
|
||||
@@ -82,7 +82,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
webcamResolutionProperty.set(WebcamResolution.HD);
|
||||
}
|
||||
|
||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), new QRScanListener(), new ScanDelayCalculator());
|
||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
|
||||
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
|
||||
webcamService.setRestartOnFailure(false);
|
||||
WebcamView webcamView = new WebcamView(webcamService);
|
||||
@@ -104,6 +104,15 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
if(percentComplete.get() <= 0.0) {
|
||||
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
|
||||
for(WebcamDevice device : WebcamScanDriver.getFoundDevices()) {
|
||||
if(device.getName().equals(Config.get().getWebcamDevice())) {
|
||||
webcamDeviceProperty.set(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
VBox vBox = new VBox(20);
|
||||
@@ -122,6 +131,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS &&
|
||||
nested.getMessage().startsWith("Library 'OpenIMAJGrabber' was not loaded successfully from file")) {
|
||||
exception = new WebcamDependencyException("Your system is missing a dependency required for the webcam. Follow the link below for more details.\n\n[https://sparrowwallet.com/docs/faq.html#your-system-is-missing-a-dependency-for-the-webcam]", exception);
|
||||
} else if(nested.getMessage().startsWith("Cannot start native grabber") && Config.get().getWebcamDevice() != null) {
|
||||
exception = new WebcamOpenException("Cannot open configured webcam " + Config.get().getWebcamDevice() + ", reverting to the default webcam");
|
||||
Config.get().setWebcamDevice(null);
|
||||
}
|
||||
|
||||
final Throwable result = exception;
|
||||
@@ -134,6 +146,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
webcamService.cancel();
|
||||
});
|
||||
webcamDeviceProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Config.get().setWebcamDevice(newValue.getName());
|
||||
if(!Objects.equals(webcamService.getDevice(), newValue)) {
|
||||
webcamService.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
|
||||
@@ -146,7 +164,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().addAll(hdButtonType, cancelButtonType);
|
||||
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2);
|
||||
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
|
||||
dialogPane.setPrefWidth(646);
|
||||
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
@@ -520,6 +539,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
public void webcamClosed(WebcamEvent webcamEvent) {
|
||||
if(webcamResolutionProperty.get() != null) {
|
||||
webcamService.setResolution(webcamResolutionProperty.get());
|
||||
webcamService.setDevice(webcamDeviceProperty.get());
|
||||
Platform.runLater(() -> {
|
||||
if(!webcamService.isRunning()) {
|
||||
webcamService.reset();
|
||||
@@ -558,10 +578,32 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
});
|
||||
|
||||
button = hd;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
|
||||
ComboBox<WebcamDevice> devicesCombo = new ComboBox<>(WebcamScanDriver.getFoundDevices());
|
||||
devicesCombo.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(WebcamDevice device) {
|
||||
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebcamDevice fromString(String string) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
});
|
||||
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
|
||||
ButtonBar.setButtonData(devicesCombo, ButtonBar.ButtonData.LEFT);
|
||||
|
||||
button = devicesCombo;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
||||
if(button instanceof Region) {
|
||||
((Region)button).setPrefWidth(150);
|
||||
((Region)button).setMaxWidth(150);
|
||||
}
|
||||
|
||||
button.disableProperty().bind(webcamService.openingProperty());
|
||||
return button;
|
||||
}
|
||||
@@ -570,7 +612,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
if(isHd) {
|
||||
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
|
||||
} else {
|
||||
hd.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
|
||||
hd.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,6 +787,24 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
}
|
||||
|
||||
public static class WebcamOpenException extends ScanException {
|
||||
public WebcamOpenException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public WebcamOpenException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public WebcamOpenException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public WebcamOpenException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ScanDelayCalculator implements WebcamUpdater.DelayCalculator {
|
||||
public long calculateDelay(long snapshotDuration, double deviceFps) {
|
||||
return Math.max(SCAN_PERIOD_MILLIS - snapshotDuration, 0L);
|
||||
|
||||
@@ -44,7 +44,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
|
||||
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
importAccordion.getPanes().add(importPane);
|
||||
}
|
||||
|
||||
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
|
||||
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
|
||||
for(WalletImport importer : walletImporters) {
|
||||
FileWalletImportPane importPane = new FileWalletImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.awt.image.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -312,6 +313,23 @@ public class WebcamScanDevice implements WebcamDevice, WebcamDevice.BufferAccess
|
||||
return this.fps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
}
|
||||
if(o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
WebcamScanDevice that = (WebcamScanDevice) o;
|
||||
return Objects.equals(fullname, that.fullname);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(fullname);
|
||||
}
|
||||
|
||||
static {
|
||||
DIMENSIONS = new Dimension[]{WebcamResolution.QQVGA.getSize(), WebcamResolution.QVGA.getSize(), WebcamResolution.VGA.getSize()};
|
||||
BAND_OFFSETS = new int[]{0, 1, 2};
|
||||
|
||||
@@ -3,20 +3,43 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.github.sarxos.webcam.WebcamDevice;
|
||||
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDevice;
|
||||
import com.github.sarxos.webcam.ds.buildin.WebcamDefaultDriver;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WebcamScanDriver extends WebcamDefaultDriver {
|
||||
private static final ObservableList<WebcamDevice> webcamDevices = FXCollections.observableArrayList();
|
||||
private static boolean rescan;
|
||||
|
||||
@Override
|
||||
public List<WebcamDevice> getDevices() {
|
||||
List<WebcamDevice> devices = super.getDevices();
|
||||
List<WebcamDevice> scanDevices = new ArrayList<>();
|
||||
for(WebcamDevice device : devices) {
|
||||
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
|
||||
scanDevices.add(new WebcamScanDevice(defaultDevice.getDeviceRef()));
|
||||
if(rescan || webcamDevices.isEmpty()) {
|
||||
List<WebcamDevice> devices = super.getDevices();
|
||||
List<WebcamDevice> scanDevices = new ArrayList<>();
|
||||
for(WebcamDevice device : devices) {
|
||||
WebcamDefaultDevice defaultDevice = (WebcamDefaultDevice)device;
|
||||
WebcamScanDevice scanDevice = new WebcamScanDevice(defaultDevice.getDeviceRef());
|
||||
if(scanDevices.stream().noneMatch(dev -> ((WebcamScanDevice)dev).getDeviceName().equals(scanDevice.getDeviceName()))) {
|
||||
scanDevices.add(scanDevice);
|
||||
}
|
||||
}
|
||||
|
||||
List<WebcamDevice> newDevices = new ArrayList<>(scanDevices);
|
||||
newDevices.removeAll(webcamDevices);
|
||||
webcamDevices.addAll(newDevices);
|
||||
webcamDevices.removeIf(device -> !scanDevices.contains(device));
|
||||
}
|
||||
|
||||
return scanDevices;
|
||||
return webcamDevices;
|
||||
}
|
||||
|
||||
public static ObservableList<WebcamDevice> getFoundDevices() {
|
||||
return webcamDevices;
|
||||
}
|
||||
|
||||
public static void rescan() {
|
||||
rescan = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.github.sarxos.webcam.Webcam;
|
||||
import com.github.sarxos.webcam.WebcamListener;
|
||||
import com.github.sarxos.webcam.WebcamResolution;
|
||||
import com.github.sarxos.webcam.WebcamUpdater;
|
||||
import com.github.sarxos.webcam.*;
|
||||
import com.google.zxing.*;
|
||||
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
|
||||
import com.google.zxing.common.HybridBinarizer;
|
||||
import com.google.zxing.qrcode.QRCodeReader;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -23,26 +22,30 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class WebcamService extends ScheduledService<Image> {
|
||||
private WebcamResolution resolution;
|
||||
private WebcamDevice device;
|
||||
private final WebcamListener listener;
|
||||
private final WebcamUpdater.DelayCalculator delayCalculator;
|
||||
private final BooleanProperty opening = new SimpleBooleanProperty(false);
|
||||
|
||||
private final ObjectProperty<Result> resultProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private static final int QR_SAMPLE_PERIOD_MILLIS = 400;
|
||||
private static final int QR_SAMPLE_PERIOD_MILLIS = 200;
|
||||
|
||||
private Webcam cam;
|
||||
private long lastQrSampleTime;
|
||||
private final Reader qrReader;
|
||||
|
||||
static {
|
||||
Webcam.setDriver(new WebcamScanDriver());
|
||||
}
|
||||
|
||||
public WebcamService(WebcamResolution resolution, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
|
||||
public WebcamService(WebcamResolution resolution, WebcamDevice device, WebcamListener listener, WebcamUpdater.DelayCalculator delayCalculator) {
|
||||
this.resolution = resolution;
|
||||
this.device = device;
|
||||
this.listener = listener;
|
||||
this.delayCalculator = delayCalculator;
|
||||
this.lastQrSampleTime = System.currentTimeMillis();
|
||||
this.qrReader = new QRCodeReader();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -58,6 +61,23 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
}
|
||||
|
||||
cam = webcams.get(0);
|
||||
|
||||
if(device != null) {
|
||||
for(Webcam webcam : webcams) {
|
||||
if(webcam.getDevice().getName().equals(device.getName())) {
|
||||
cam = webcam;
|
||||
}
|
||||
}
|
||||
} else if(Config.get().getWebcamDevice() != null) {
|
||||
for(Webcam webcam : webcams) {
|
||||
if(webcam.getDevice().getName().equals(Config.get().getWebcamDevice())) {
|
||||
cam = webcam;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
device = cam.getDevice();
|
||||
|
||||
cam.setCustomViewSizes(resolution.getSize());
|
||||
cam.setViewSize(resolution.getSize());
|
||||
if(!Arrays.asList(cam.getWebcamListeners()).contains(listener)) {
|
||||
@@ -70,6 +90,10 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
}
|
||||
|
||||
BufferedImage bimg = cam.getImage();
|
||||
if(bimg == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Image image = SwingFXUtils.toFXImage(bimg, null);
|
||||
updateValue(image);
|
||||
|
||||
@@ -106,9 +130,9 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
try {
|
||||
Result result = new MultiFormatReader().decode(bitmap);
|
||||
Result result = qrReader.decode(bitmap);
|
||||
resultProperty.set(result);
|
||||
} catch(NotFoundException e) {
|
||||
} catch(ReaderException e) {
|
||||
// fall thru, it means there is no QR code in image
|
||||
}
|
||||
}
|
||||
@@ -133,6 +157,14 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
this.resolution = resolution;
|
||||
}
|
||||
|
||||
public WebcamDevice getDevice() {
|
||||
return device;
|
||||
}
|
||||
|
||||
public void setDevice(WebcamDevice device) {
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
public boolean isOpening() {
|
||||
return opening.get();
|
||||
}
|
||||
|
||||
@@ -3,15 +3,17 @@ package com.sparrowwallet.sparrow.event;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletUtxoStatusChangedEvent extends WalletChangedEvent {
|
||||
private final BlockTransactionHashIndex utxo;
|
||||
import java.util.List;
|
||||
|
||||
public WalletUtxoStatusChangedEvent(Wallet wallet, BlockTransactionHashIndex utxo) {
|
||||
public class WalletUtxoStatusChangedEvent extends WalletChangedEvent {
|
||||
private final List<BlockTransactionHashIndex> utxos;
|
||||
|
||||
public WalletUtxoStatusChangedEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
super(wallet);
|
||||
this.utxo = utxo;
|
||||
this.utxos = utxos;
|
||||
}
|
||||
|
||||
public BlockTransactionHashIndex getUtxo() {
|
||||
return utxo;
|
||||
public List<BlockTransactionHashIndex> getUtxos() {
|
||||
return utxos;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
private static final Logger log = LoggerFactory.getLogger(ColdcardMultisig.class);
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Caravan Multisig";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.CARAVAN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return "Import the file created via the Download Wallet Details button in Caravan.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||
CaravanFile cf = JsonPersistence.getGson().fromJson(reader, CaravanFile.class);
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(cf.name);
|
||||
wallet.setPolicyType(PolicyType.MULTI);
|
||||
|
||||
for(ExtPublicKey extKey : cf.extendedPublicKeys) {
|
||||
Keystore keystore = new Keystore(extKey.name);
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(extKey.xpub));
|
||||
|
||||
WalletModel walletModel = WalletModel.fromType(extKey.method);
|
||||
if(walletModel == null) {
|
||||
keystore.setWalletModel(WalletModel.SPARROW);
|
||||
keystore.setSource(KeystoreSource.SW_WATCH);
|
||||
} else {
|
||||
keystore.setWalletModel(walletModel);
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
}
|
||||
wallet.getKeystores().add(keystore);
|
||||
}
|
||||
|
||||
ScriptType scriptType = ScriptType.valueOf(cf.addressType);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), cf.quorum.requiredSigners));
|
||||
|
||||
return wallet;
|
||||
} catch(Exception e) {
|
||||
log.error("Error importing " + getName() + " wallet", e);
|
||||
throw new ImportException("Error importing " + getName() + " wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
if(!wallet.isValid()) {
|
||||
throw new ExportException("Cannot export an incomplete wallet");
|
||||
}
|
||||
|
||||
if(!wallet.getPolicyType().equals(PolicyType.MULTI)) {
|
||||
throw new ExportException(getName() + " import requires a multisig wallet");
|
||||
}
|
||||
|
||||
try {
|
||||
CaravanFile cf = new CaravanFile();
|
||||
cf.name = wallet.getName();
|
||||
cf.addressType = wallet.getScriptType().toString().replace('-', '_');
|
||||
cf.network = Network.get().getName();
|
||||
cf.client = new Client();
|
||||
|
||||
Quorum quorum = new Quorum();
|
||||
quorum.requiredSigners = wallet.getDefaultPolicy().getNumSignaturesRequired();
|
||||
quorum.totalSigners = wallet.getKeystores().size();
|
||||
cf.quorum = quorum;
|
||||
|
||||
cf.extendedPublicKeys = new ArrayList<>();
|
||||
for(Keystore keystore : wallet.getKeystores()) {
|
||||
ExtPublicKey extKey = new ExtPublicKey();
|
||||
extKey.name = keystore.getLabel();
|
||||
extKey.bip32Path = keystore.getKeyDerivation().getDerivationPath();
|
||||
extKey.xpub = keystore.getExtendedPublicKey().toString();
|
||||
extKey.xfp = keystore.getKeyDerivation().getMasterFingerprint();
|
||||
extKey.method = keystore.getWalletModel().getType();
|
||||
cf.extendedPublicKeys.add(extKey);
|
||||
}
|
||||
|
||||
Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
String json = gson.toJson(cf);
|
||||
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.flush();
|
||||
} catch(Exception e) {
|
||||
log.error("Error exporting " + getName() + " wallet", e);
|
||||
throw new ExportException("Error exporting " + getName() + " wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "Export a file that can be imported via the Import Wallet Configuration button in Caravan.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
return "json";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean walletExportRequiresDecryption() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted(File file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final class CaravanFile {
|
||||
public String name;
|
||||
public String addressType;
|
||||
public String network;
|
||||
public Client client;
|
||||
public Quorum quorum;
|
||||
public List<ExtPublicKey> extendedPublicKeys;
|
||||
public int startingAddressIndex = 0;
|
||||
}
|
||||
|
||||
private static final class Client {
|
||||
public String type = "public";
|
||||
}
|
||||
|
||||
private static final class Quorum {
|
||||
public int requiredSigners;
|
||||
public int totalSigners;
|
||||
}
|
||||
|
||||
private static final class ExtPublicKey {
|
||||
public String name;
|
||||
public String bip32Path;
|
||||
public String xpub;
|
||||
public String xfp;
|
||||
public String method;
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ public class Config {
|
||||
private Integer keyDerivationPeriod;
|
||||
private File hwi;
|
||||
private Boolean hdCapture;
|
||||
private String webcamDevice;
|
||||
private ServerType serverType;
|
||||
private String publicElectrumServer;
|
||||
private String coreServer;
|
||||
@@ -307,6 +308,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public String getWebcamDevice() {
|
||||
return webcamDevice;
|
||||
}
|
||||
|
||||
public void setWebcamDevice(String webcamDevice) {
|
||||
this.webcamDevice = webcamDevice;
|
||||
flush();
|
||||
}
|
||||
|
||||
public ServerType getServerType() {
|
||||
return serverType;
|
||||
}
|
||||
|
||||
@@ -119,8 +119,8 @@ public class Hwi {
|
||||
|
||||
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) throws DisplayAddressException {
|
||||
try {
|
||||
if(!Arrays.asList(ScriptType.SINGLE_HASH_TYPES).contains(scriptType)) {
|
||||
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only single hash types supported");
|
||||
if(!Arrays.asList(ScriptType.ADDRESSABLE_TYPES).contains(scriptType)) {
|
||||
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only addressable types supported");
|
||||
}
|
||||
|
||||
String descriptor = outputDescriptor.toString();
|
||||
@@ -254,6 +254,7 @@ public class Hwi {
|
||||
|
||||
File hwiHomeDir = getHwiHomeDir();
|
||||
File hwiVersionDir = new File(hwiHomeDir, HWI_VERSION_DIR);
|
||||
IOUtils.deleteDirectory(hwiVersionDir);
|
||||
if(!hwiVersionDir.exists()) {
|
||||
Files.createDirectories(hwiVersionDir.toPath(), PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
}
|
||||
|
||||
@@ -90,7 +90,9 @@ public class IOUtils {
|
||||
// if it is a subdirectory, we just return the directory name
|
||||
entry = entry.substring(0, checkSubdir);
|
||||
}
|
||||
result.add(entry);
|
||||
if(!entry.isEmpty()) {
|
||||
result.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -237,8 +237,20 @@ public class DbPersistence implements Persistence {
|
||||
for(Entry entry : dirtyPersistables.labelEntries) {
|
||||
if(entry instanceof TransactionEntry && ((TransactionEntry)entry).getBlockTransaction().getId() != null) {
|
||||
blockTransactionDao.updateLabel(((TransactionEntry)entry).getBlockTransaction().getId(), entry.getLabel());
|
||||
} else if(entry instanceof NodeEntry && ((NodeEntry)entry).getNode().getId() != null) {
|
||||
walletNodeDao.updateNodeLabel(((NodeEntry)entry).getNode().getId(), entry.getLabel());
|
||||
} else if(entry instanceof NodeEntry) {
|
||||
WalletNode addressNode = ((NodeEntry)entry).getNode();
|
||||
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);
|
||||
purposeNode.setId(purposeNodeId);
|
||||
}
|
||||
|
||||
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId());
|
||||
addressNode.setId(nodeId);
|
||||
}
|
||||
|
||||
walletNodeDao.updateNodeLabel(addressNode.getId(), entry.getLabel());
|
||||
} else if(entry instanceof HashIndexEntry && ((HashIndexEntry)entry).getHashIndex().getId() != null) {
|
||||
walletNodeDao.updateTxoLabel(((HashIndexEntry)entry).getHashIndex().getId(), entry.getLabel());
|
||||
}
|
||||
@@ -610,7 +622,7 @@ public class DbPersistence implements Persistence {
|
||||
@Subscribe
|
||||
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).utxoStatuses.add(event.getUtxo());
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).utxoStatuses.addAll(event.getUtxos());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.github.arteam.simplejsonrpc.client.Transport;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
|
||||
+2
-1
@@ -22,7 +22,6 @@ import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import net.freehaven.tor.control.TorControlError;
|
||||
import org.berndpruenster.netlayer.tor.Tor;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
@@ -573,6 +572,8 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
reason += ". Check if the proxy server is running.";
|
||||
} else if(exception instanceof TorServerAlreadyBoundException) {
|
||||
reason += "\nIs a Tor proxy already running on port " + TorService.PROXY_PORT + "?";
|
||||
} else if(reason != null && reason.contains("Check if Bitcoin Core is running")) {
|
||||
reason += "\n\nSee https://sparrowwallet.com/docs/connect-node.html";
|
||||
}
|
||||
|
||||
testResults.setText("Could not connect:\n\n" + reason);
|
||||
|
||||
@@ -19,7 +19,6 @@ import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.payjoin.Payjoin;
|
||||
import com.sparrowwallet.sparrow.payjoin.PayjoinReceiverException;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
@@ -410,10 +409,10 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
psbtSigHash = psbtInput.getSigHash();
|
||||
}
|
||||
}
|
||||
sigHash.setValue(psbtSigHash);
|
||||
sigHash.setValue(psbtSigHash == SigHash.ALL_TAPROOT ? SigHash.ALL : psbtSigHash);
|
||||
sigHash.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||
psbtInput.setSigHash(newValue);
|
||||
psbtInput.setSigHash(psbtInput.isTaproot() && newValue == SigHash.ALL ? SigHash.ALL_TAPROOT : newValue);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -443,9 +442,6 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
String type = "Legacy";
|
||||
if(headersForm.getTransaction().isSegwit() || (headersForm.getPsbt() != null && headersForm.getPsbt().getPsbtInputs().stream().anyMatch(in -> in.getWitnessUtxo() != null))) {
|
||||
type = "Segwit";
|
||||
if(headersForm.getTransaction().getSegwitVersion() == 2) {
|
||||
type = "Taproot";
|
||||
}
|
||||
}
|
||||
segwit.setText(type);
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public class InputForm extends IndexedTransactionForm {
|
||||
|
||||
public boolean isWalletTxo() {
|
||||
TransactionInput txInput = getTransactionInput();
|
||||
return getSigningWallet() != null && getSigningWallet().getWalletTxos().keySet().stream().anyMatch(ref -> ref.getHash().equals(txInput.getOutpoint().getHash()) && ref.getIndex() == txInput.getOutpoint().getIndex());
|
||||
return getSigningWallet() != null && getSigningWallet().isWalletTxo(txInput);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,9 +8,7 @@ import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.MaxUtxoSelector;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.UtxoSelector;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -21,8 +19,11 @@ import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
@@ -65,12 +66,19 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
@FXML
|
||||
private Label amountStatus;
|
||||
|
||||
@FXML
|
||||
private Label dustStatus;
|
||||
|
||||
@FXML
|
||||
private ToggleButton maxButton;
|
||||
|
||||
@FXML
|
||||
private Button addPaymentButton;
|
||||
|
||||
private final BooleanProperty emptyAmountProperty = new SimpleBooleanProperty(true);
|
||||
|
||||
private final BooleanProperty dustAmountProperty = new SimpleBooleanProperty();
|
||||
|
||||
private final ChangeListener<String> amountListener = new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
||||
@@ -86,8 +94,12 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
Long recipientValueSats = getRecipientValueSats();
|
||||
if(recipientValueSats != null) {
|
||||
setFiatAmount(AppServices.getFiatCurrencyExchangeRate(), recipientValueSats);
|
||||
dustAmountProperty.set(recipientValueSats <= getRecipientDustThreshold());
|
||||
emptyAmountProperty.set(false);
|
||||
} else {
|
||||
fiatAmount.setText("");
|
||||
dustAmountProperty.set(false);
|
||||
emptyAmountProperty.set(true);
|
||||
}
|
||||
|
||||
sendController.updateTransaction();
|
||||
@@ -115,8 +127,8 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
//ignore, not a URI
|
||||
}
|
||||
|
||||
revalidate(amount, amountListener);
|
||||
maxButton.setDisable(!isValidAddressAndLabel());
|
||||
revalidateAmount();
|
||||
maxButton.setDisable(!isMaxButtonEnabled());
|
||||
sendController.updateTransaction();
|
||||
|
||||
if(validationSupport != null) {
|
||||
@@ -125,7 +137,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
});
|
||||
|
||||
label.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
maxButton.setDisable(!isValidAddressAndLabel());
|
||||
maxButton.setDisable(!isMaxButtonEnabled());
|
||||
sendController.getCreateButton().setDisable(sendController.getWalletTransaction() == null || newValue == null || newValue.isEmpty() || sendController.isInsufficientFeeRate());
|
||||
sendController.updateTransaction();
|
||||
});
|
||||
@@ -143,15 +155,20 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
}
|
||||
});
|
||||
|
||||
maxButton.setDisable(!isValidAddressAndLabel());
|
||||
maxButton.setDisable(!isMaxButtonEnabled());
|
||||
sendController.utxoSelectorProperty().addListener((observable, oldValue, newValue) -> {
|
||||
maxButton.setDisable(!isMaxButtonEnabled());
|
||||
});
|
||||
sendController.getPaymentTabs().getTabs().addListener((ListChangeListener<Tab>) c -> {
|
||||
maxButton.setDisable(!isMaxButtonEnabled());
|
||||
});
|
||||
sendController.utxoLabelSelectionProperty().addListener((observable, oldValue, newValue) -> {
|
||||
maxButton.setText("Max" + newValue);
|
||||
});
|
||||
amountStatus.managedProperty().bind(amountStatus.visibleProperty());
|
||||
amountStatus.setVisible(sendController.isInsufficientInputs());
|
||||
sendController.insufficientInputsProperty().addListener((observable, oldValue, newValue) -> {
|
||||
amountStatus.setVisible(newValue);
|
||||
});
|
||||
amountStatus.visibleProperty().bind(sendController.insufficientInputsProperty().and(dustAmountProperty.not()).and(emptyAmountProperty.not()));
|
||||
dustStatus.managedProperty().bind(dustStatus.visibleProperty());
|
||||
dustStatus.visibleProperty().bind(dustAmountProperty);
|
||||
|
||||
Optional<Tab> firstTab = sendController.getPaymentTabs().getTabs().stream().findFirst();
|
||||
if(firstTab.isPresent()) {
|
||||
@@ -173,7 +190,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
Validator.createEmptyValidator("Label is required")
|
||||
));
|
||||
validationSupport.registerValidator(amount, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", sendController.isInsufficientInputs()),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Inputs", getRecipientValueSats() != null && sendController.isInsufficientInputs()),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Insufficient Value", getRecipientValueSats() != null && getRecipientValueSats() <= getRecipientDustThreshold())
|
||||
));
|
||||
}
|
||||
@@ -191,6 +208,10 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
return isValidRecipientAddress() && !label.getText().isEmpty();
|
||||
}
|
||||
|
||||
private boolean isMaxButtonEnabled() {
|
||||
return isValidAddressAndLabel() || (sendController.utxoSelectorProperty().get() instanceof PresetUtxoSelector && sendController.getPaymentTabs().getTabs().size() == 1);
|
||||
}
|
||||
|
||||
private Address getRecipientAddress() throws InvalidAddressException {
|
||||
return Address.fromString(address.getText());
|
||||
}
|
||||
@@ -234,8 +255,11 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
}
|
||||
}
|
||||
|
||||
public void revalidate() {
|
||||
public void revalidateAmount() {
|
||||
revalidate(amount, amountListener);
|
||||
Long recipientValueSats = getRecipientValueSats();
|
||||
dustAmountProperty.set(recipientValueSats != null && recipientValueSats <= getRecipientDustThreshold());
|
||||
emptyAmountProperty.set(recipientValueSats == null);
|
||||
}
|
||||
|
||||
private void revalidate(TextField field, ChangeListener<String> listener) {
|
||||
@@ -302,7 +326,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
fiatAmount.setText("");
|
||||
setSendMax(false);
|
||||
|
||||
amountStatus.setVisible(false);
|
||||
dustAmountProperty.set(false);
|
||||
}
|
||||
|
||||
public void setMaxInput(ActionEvent event) {
|
||||
@@ -310,6 +334,11 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
if(utxoSelector == null) {
|
||||
MaxUtxoSelector maxUtxoSelector = new MaxUtxoSelector();
|
||||
sendController.utxoSelectorProperty().set(maxUtxoSelector);
|
||||
} else if(utxoSelector instanceof PresetUtxoSelector && !isValidAddressAndLabel() && sendController.getPaymentTabs().getTabs().size() == 1) {
|
||||
PresetUtxoSelector presetUtxoSelector = (PresetUtxoSelector)utxoSelector;
|
||||
Payment payment = new Payment(null, null, presetUtxoSelector.getPresetUtxos().stream().mapToLong(BlockTransactionHashIndex::getValue).sum(), true);
|
||||
setPayment(payment);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -170,7 +170,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
userFeeSet.set(false);
|
||||
for(Tab tab : paymentTabs.getTabs()) {
|
||||
PaymentController controller = (PaymentController)tab.getUserData();
|
||||
controller.revalidate();
|
||||
controller.revalidateAmount();
|
||||
}
|
||||
updateTransaction();
|
||||
}
|
||||
@@ -183,7 +183,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
userFeeSet.set(false);
|
||||
for(Tab tab : paymentTabs.getTabs()) {
|
||||
PaymentController controller = (PaymentController)tab.getUserData();
|
||||
controller.revalidate();
|
||||
controller.revalidateAmount();
|
||||
}
|
||||
updateTransaction();
|
||||
}
|
||||
@@ -228,7 +228,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
insufficientInputsProperty.addListener((observable, oldValue, newValue) -> {
|
||||
for(Tab tab : paymentTabs.getTabs()) {
|
||||
PaymentController controller = (PaymentController)tab.getUserData();
|
||||
controller.revalidate();
|
||||
controller.revalidateAmount();
|
||||
}
|
||||
revalidate(fee, feeListener);
|
||||
});
|
||||
@@ -905,6 +905,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
excludedChangeNodes.clear();
|
||||
walletTransactionProperty.setValue(null);
|
||||
createdWalletTransactionProperty.set(null);
|
||||
insufficientInputsProperty.set(false);
|
||||
|
||||
validationSupport.setErrorDecorationEnabled(false);
|
||||
}
|
||||
@@ -1163,7 +1164,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
updateTransaction(true);
|
||||
} else if(utxoSelectorProperty().get() instanceof PresetUtxoSelector) {
|
||||
PresetUtxoSelector presetUtxoSelector = new PresetUtxoSelector(((PresetUtxoSelector)utxoSelector).getPresetUtxos());
|
||||
presetUtxoSelector.getPresetUtxos().remove(event.getUtxo());
|
||||
presetUtxoSelector.getPresetUtxos().removeAll(event.getUtxos());
|
||||
utxoSelectorProperty.set(presetUtxoSelector);
|
||||
updateTransaction(true);
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
open module com.sparrowwallet.sparrow {
|
||||
requires java.desktop;
|
||||
requires java.net.http;
|
||||
requires javafx.base;
|
||||
requires javafx.controls;
|
||||
requires javafx.fxml;
|
||||
requires javafx.graphics;
|
||||
@@ -10,7 +11,6 @@ open module com.sparrowwallet.sparrow {
|
||||
requires tornadofx.controls;
|
||||
requires com.sparrowwallet.drongo;
|
||||
requires com.google.common;
|
||||
requires flowless;
|
||||
requires com.google.zxing;
|
||||
requires com.google.zxing.javase;
|
||||
requires simple.json.rpc.client;
|
||||
@@ -18,21 +18,23 @@ open module com.sparrowwallet.sparrow {
|
||||
requires simple.json.rpc.core;
|
||||
requires org.jetbrains.annotations;
|
||||
requires com.fasterxml.jackson.databind;
|
||||
requires webcam.capture;
|
||||
requires netlayer.jpms;
|
||||
requires hummingbird;
|
||||
requires centerdevice.nsmenufx;
|
||||
requires jcommander;
|
||||
requires org.slf4j;
|
||||
requires bwt.jni;
|
||||
requires jtorctl;
|
||||
requires javacsv;
|
||||
requires jul.to.slf4j;
|
||||
requires bridj;
|
||||
requires com.google.gson;
|
||||
requires org.jdbi.v3.core;
|
||||
requires org.jdbi.v3.sqlobject;
|
||||
requires org.flywaydb.core;
|
||||
requires com.zaxxer.hikari;
|
||||
requires com.h2database;
|
||||
requires com.sparrowwallet.hummingbird;
|
||||
requires org.fxmisc.flowless;
|
||||
requires com.github.sarxos.webcam.capture;
|
||||
requires centerdevice.nsmenufx;
|
||||
requires com.github.JesusMcCloud.jtorctl;
|
||||
requires com.beust.jcommander;
|
||||
requires org.slf4j.jul.to.slf4j;
|
||||
requires net.sourceforge.javacsv;
|
||||
requires com.nativelibs4java.bridj;
|
||||
requires org.reactfx.reactfx;
|
||||
requires dev.bwt.jni;
|
||||
}
|
||||
@@ -65,6 +65,14 @@
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_CIRCLE" styleClass="failure" />
|
||||
</graphic>
|
||||
</Label>
|
||||
<Label fx:id="dustStatus" text="Amount too low">
|
||||
<tooltip>
|
||||
<Tooltip text="Amount below the Bitcoin network dust threshold for this address type" />
|
||||
</tooltip>
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_CIRCLE" styleClass="failure" />
|
||||
</graphic>
|
||||
</Label>
|
||||
</Group>
|
||||
<Region style="-fx-pref-width: 5" />
|
||||
<ToggleButton fx:id="maxButton" text="Max" onAction="#setMaxInput" />
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
@@ -0,0 +1,44 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class CaravanMultisigTest extends IoTest {
|
||||
@Test
|
||||
public void importWallet1() throws ImportException {
|
||||
CaravanMultisig ccMultisig = new CaravanMultisig();
|
||||
Wallet wallet = ccMultisig.importWallet(getInputStream("caravan-multisig-export-1.json"), null);
|
||||
Assert.assertEquals("Test Wallet", wallet.getName());
|
||||
Assert.assertEquals(PolicyType.MULTI, wallet.getPolicyType());
|
||||
Assert.assertEquals(ScriptType.P2WSH, wallet.getScriptType());
|
||||
Assert.assertEquals(2, wallet.getDefaultPolicy().getNumSignaturesRequired());
|
||||
Assert.assertEquals("wsh(sortedmulti(2,mercury,venus,earth))", wallet.getDefaultPolicy().getMiniscript().getScript().toLowerCase());
|
||||
Assert.assertTrue(wallet.isValid());
|
||||
Assert.assertEquals("8188029f", wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
||||
Assert.assertEquals("m/48'/0'/0'/2'", wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath());
|
||||
Assert.assertEquals(WalletModel.TREZOR_1, wallet.getKeystores().get(0).getWalletModel());
|
||||
Assert.assertEquals("xpub6EMVvcTUbaABdaPLaVWE72CjcN72URa5pKK1knrKLz1hKaDwUkgddc3832a8MHEpLyuow7MfjMRomt2iMtwPH4pWrFLft4JsquHjeZfKsYp", wallet.getKeystores().get(0).getExtendedPublicKey().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void exportWallet1() throws ImportException, ExportException, IOException {
|
||||
CaravanMultisig ccMultisig = new CaravanMultisig();
|
||||
byte[] walletBytes = ByteStreams.toByteArray(getInputStream("caravan-multisig-export-1.json"));
|
||||
Wallet wallet = ccMultisig.importWallet(new ByteArrayInputStream(walletBytes), null);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ccMultisig.exportWallet(wallet, baos);
|
||||
byte[] exportedBytes = baos.toByteArray();
|
||||
String original = new String(walletBytes);
|
||||
String exported = new String(exportedBytes);
|
||||
Assert.assertEquals(original, exported);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,23 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
|
||||
public class IoTest {
|
||||
public static final String IO_TEST_PATH = "/com/sparrowwallet/sparrow/io/";
|
||||
|
||||
protected File getFile(String filename) {
|
||||
return new File(this.getClass().getResource(IO_TEST_PATH + filename).getFile());
|
||||
try {
|
||||
Path tempFile = Files.createTempFile(filename, null);
|
||||
Files.copy(getInputStream(filename), tempFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
return tempFile.toFile();
|
||||
} catch(IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected InputStream getInputStream(String filename) {
|
||||
|
||||
@@ -57,7 +57,7 @@ public class StorageTest extends IoTest {
|
||||
|
||||
@Test
|
||||
public void multipleLoadTest() throws IOException, MnemonicException, StorageException {
|
||||
for(int i = 0; i < 100; i++) {
|
||||
for(int i = 0; i < 5; i++) {
|
||||
loadSeedWallet();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "Test Wallet",
|
||||
"addressType": "P2WSH",
|
||||
"network": "mainnet",
|
||||
"client": {
|
||||
"type": "public"
|
||||
},
|
||||
"quorum": {
|
||||
"requiredSigners": 2,
|
||||
"totalSigners": 3
|
||||
},
|
||||
"extendedPublicKeys": [
|
||||
{
|
||||
"name": "Mercury",
|
||||
"bip32Path": "m/48'/0'/0'/2'",
|
||||
"xpub": "xpub6EMVvcTUbaABdaPLaVWE72CjcN72URa5pKK1knrKLz1hKaDwUkgddc3832a8MHEpLyuow7MfjMRomt2iMtwPH4pWrFLft4JsquHjeZfKsYp",
|
||||
"xfp": "8188029f",
|
||||
"method": "trezor"
|
||||
},
|
||||
{
|
||||
"name": "Venus",
|
||||
"bip32Path": "m/48'/0'/0'/2'",
|
||||
"xpub": "xpub6EbTHAgvzZtVppjLPXia2prt6T2w7DD2VQ8gysgV6ECzmKkA6zt4WR3hX4bgciTDrnaneZoJbA19tYKBMrWuA89SbgYAdbMNWmtzrgLhqco",
|
||||
"xfp": "a15e8133",
|
||||
"method": "ledger"
|
||||
},
|
||||
{
|
||||
"name": "Earth",
|
||||
"bip32Path": "m/48'/0'/0'/2'",
|
||||
"xpub": "xpub6Ea6qWGr6BWpCSyLo8ggypvXg1MwadiAGZxAHvrBUwvT2ki6pNVG61bg3YEdxkj7FRJ1cLFK7Vvqd9rcDvwCLX7VfEMfJJfdbA1NGsFazvz",
|
||||
"xfp": "d31cf7f9",
|
||||
"method": "coldcard"
|
||||
}
|
||||
],
|
||||
"startingAddressIndex": 0
|
||||
}
|
||||
Reference in New Issue
Block a user