mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +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 | ||
|
|
c8d997fbf0 | ||
|
|
911ed3a718 | ||
|
|
dbfed31432 | ||
|
|
6f3d4e224e | ||
|
|
4d6609990c | ||
|
|
5482196cc7 | ||
|
|
09f6c9ef81 | ||
|
|
9b8f97c041 | ||
|
|
c68c713a4b | ||
|
|
02e144f802 | ||
|
|
a9ab4d6c78 | ||
|
|
5df4e5761c | ||
|
|
6d8b8579ba | ||
|
|
eaa5190502 | ||
|
|
445db6a4d6 | ||
|
|
7f178b5f67 | ||
|
|
1208baf00e | ||
|
|
655a473cd5 | ||
|
|
e6c536930b | ||
|
|
f1510de360 | ||
|
|
cfac2768ae | ||
|
|
ab41f2e80e | ||
|
|
9ebabecfbe | ||
|
|
8914acff68 | ||
|
|
4a0ecba716 | ||
|
|
e99b1d4171 | ||
|
|
a59d5d3086 | ||
|
|
600a77da3a | ||
|
|
bc83f6fa22 | ||
|
|
4cbde7e7aa | ||
|
|
3ae63408e6 | ||
|
|
e740c6d162 | ||
|
|
f7f5852476 | ||
|
|
447e2ab264 | ||
|
|
8a77f22158 | ||
|
|
c096327be4 | ||
|
|
911153e1aa | ||
|
|
a60eadf8fc | ||
|
|
9ebbf2557f | ||
|
|
42b279d22a | ||
|
|
1a452db4cf | ||
|
|
c1cf5be616 | ||
|
|
dd146210ba | ||
|
|
94088f795c | ||
|
|
c5b09189df | ||
|
|
197c44bb07 | ||
|
|
c202a941b9 | ||
|
|
383594b03c | ||
|
|
42bfe572ef | ||
|
|
1677c47500 | ||
|
|
53a447c72d | ||
|
|
f23a891ece | ||
|
|
0e42c657b3 | ||
|
|
b17c15f702 | ||
|
|
c9cdf6e77d | ||
|
|
4f6ead842d | ||
|
|
adcddfa84d | ||
|
|
d67c5c5218 | ||
|
|
fb72010bdf | ||
|
|
8bc8bdb2f2 | ||
|
|
8f4cf9f2a0 | ||
|
|
ed323a8388 | ||
|
|
d559b437b3 | ||
|
|
6155306acc | ||
|
|
6f35d86890 | ||
|
|
cbf32a36c6 | ||
|
|
9789e0f198 | ||
|
|
574209c837 | ||
|
|
74c83fc5e1 | ||
|
|
a4ddc11bab | ||
|
|
3bf480234f | ||
|
|
29811ca5de | ||
|
|
b7522420cf | ||
|
|
f7d1e22e4e | ||
|
|
e0c38031b1 | ||
|
|
e046512e86 | ||
|
|
7cba0de268 | ||
|
|
425e476f20 | ||
|
|
e5dd33d5a1 | ||
|
|
07012615ff | ||
|
|
23f3ae51db | ||
|
|
10514b8039 | ||
|
|
19637fd706 | ||
|
|
dd56e2b42e | ||
|
|
e0b6530275 | ||
|
|
a0d3f3b745 | ||
|
|
438c13fe2d | ||
|
|
c067300ee8 | ||
|
|
020ca2f312 | ||
|
|
5ede1dd97d | ||
|
|
45f6ae214a | ||
|
|
45a9093055 |
@@ -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
|
||||
|
||||
+241
-21
@@ -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.0'
|
||||
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' ]
|
||||
}
|
||||
|
||||
@@ -44,23 +44,39 @@ dependencies {
|
||||
}
|
||||
implementation('com.google.guava:guava:28.2-jre')
|
||||
implementation('com.google.code.gson:gson:2.8.6')
|
||||
implementation('com.h2database:h2:1.4.201-SNAPSHOT')
|
||||
implementation('com.zaxxer:HikariCP:4.0.3')
|
||||
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0')
|
||||
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.0.2' ) {
|
||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
exclude group: 'org.openjfx', module: 'javafx-graphics'
|
||||
exclude group: 'org.openjfx', module: 'javafx-controls'
|
||||
@@ -69,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'
|
||||
@@ -77,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 {
|
||||
@@ -105,6 +124,7 @@ run {
|
||||
"--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls",
|
||||
"--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=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||
@@ -127,9 +147,11 @@ 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'
|
||||
}
|
||||
|
||||
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']
|
||||
@@ -143,15 +165,19 @@ jlink {
|
||||
"--add-opens=javafx.base/com.sun.javafx.event=org.controlsfx.controls",
|
||||
"--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=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=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||
"--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.desktop",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.sql",
|
||||
"--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"
|
||||
@@ -164,9 +190,9 @@ jlink {
|
||||
appVersion = "${sparrowVersion}"
|
||||
skipInstaller = os.macOsX
|
||||
imageOptions = []
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/associations.properties', '--license-file', 'LICENSE']
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/aopp.properties', '--license-file', 'LICENSE']
|
||||
if(os.windows) {
|
||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut']
|
||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
||||
installerType = "exe"
|
||||
}
|
||||
@@ -203,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: db9617ee10...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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
mime-type=x-scheme-handler/aopp
|
||||
description=Verify Address Ownership URI
|
||||
@@ -1,3 +0,0 @@
|
||||
extension=psbt
|
||||
mime-type=application/octet-stream
|
||||
description=Partially Signed Bitcoin Transaction
|
||||
@@ -0,0 +1,2 @@
|
||||
mime-type=x-scheme-handler/bitcoin
|
||||
description=Bitcoin Scheme URI
|
||||
@@ -0,0 +1,9 @@
|
||||
[Desktop Entry]
|
||||
Name=Sparrow
|
||||
Comment=Sparrow
|
||||
Exec=/opt/sparrow/bin/Sparrow %U
|
||||
Icon=/opt/sparrow/lib/Sparrow.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Unknown
|
||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/aopp
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.0</string>
|
||||
<string>1.4.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
@@ -35,5 +35,88 @@
|
||||
<string>true</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Sparrow requires access to the camera in order to scan QR codes</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.sparrowwallet.sparrow.bitcoin</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>bitcoin</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.sparrowwallet.sparrow.aopp</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>aopp</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.bitcoin.psbt</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>psbt</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Partially Signed Bitcoin Transaction</string>
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>sparrow.icns</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.bitcoin.txn</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>txn</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Bitcoin Transaction</string>
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>sparrow.icns</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>sparrow.icns</string>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>psbt</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Partially Signed Bitcoin Transaction</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>sparrow.icns</string>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>txn</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Bitcoin Transaction</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,163 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
||||
|
||||
<?ifdef JpIsSystemWide ?>
|
||||
<?define JpInstallScope="perMachine"?>
|
||||
<?else?>
|
||||
<?define JpInstallScope="perUser"?>
|
||||
<?endif?>
|
||||
|
||||
<?define JpProductLanguage=1033 ?>
|
||||
<?define JpInstallerVersion=200 ?>
|
||||
<?define JpCompressedMsi=yes ?>
|
||||
|
||||
<?include $(var.JpConfigDir)/overrides.wxi ?>
|
||||
|
||||
<Product
|
||||
Id="$(var.JpProductCode)"
|
||||
Name="$(var.JpAppName)"
|
||||
Language="$(var.JpProductLanguage)"
|
||||
Version="$(var.JpAppVersion)"
|
||||
Manufacturer="$(var.JpAppVendor)"
|
||||
UpgradeCode="$(var.JpProductUpgradeCode)">
|
||||
|
||||
<Package
|
||||
Description="$(var.JpAppDescription)"
|
||||
Manufacturer="$(var.JpAppVendor)"
|
||||
InstallerVersion="$(var.JpInstallerVersion)"
|
||||
Compressed="$(var.JpCompressedMsi)"
|
||||
InstallScope="$(var.JpInstallScope)" Platform="x64"
|
||||
/>
|
||||
|
||||
<Media Id="1" Cabinet="Data.cab" EmbedCab="yes" />
|
||||
|
||||
<?ifdef JpAllowDowngrades ?>
|
||||
<?ifdef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="yes"
|
||||
Disallow="no"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
|
||||
<?ifdef JpAllowDowngrades ?>
|
||||
<?ifndef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="yes"
|
||||
Disallow="yes"
|
||||
DisallowUpgradeErrorMessage="!(loc.DisallowUpgradeErrorMessage)"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
|
||||
<?ifndef JpAllowDowngrades ?>
|
||||
<?ifdef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="no"
|
||||
Disallow="no"
|
||||
DowngradeErrorMessage="!(loc.DowngradeErrorMessage)"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
|
||||
<?ifndef JpAllowDowngrades ?>
|
||||
<?ifndef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="no"
|
||||
Disallow="yes"
|
||||
DowngradeErrorMessage="!(loc.DowngradeErrorMessage)"
|
||||
DisallowUpgradeErrorMessage="!(loc.DisallowUpgradeErrorMessage)"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
|
||||
<!-- Standard required root -->
|
||||
<Directory Id="TARGETDIR" Name="SourceDir"/>
|
||||
|
||||
<DirectoryRef Id="TARGETDIR">
|
||||
<Component Id="RegistryEntries" Guid="{206C911C-56EF-44B8-9257-5FD214427965}">
|
||||
<RegistryKey Root="HKCR" Key="aopp" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:Address Ownership Proof Protocol"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="HKCR" Key="bitcoin" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:Bitcoin Payment URL"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<Feature Id="DefaultFeature" Title="!(loc.MainFeatureTitle)" Level="1">
|
||||
<ComponentGroupRef Id="Shortcuts"/>
|
||||
<ComponentGroupRef Id="Files"/>
|
||||
<ComponentGroupRef Id="FileAssociations"/>
|
||||
<ComponentRef Id="RegistryEntries"/>
|
||||
</Feature>
|
||||
|
||||
<?ifdef JpInstallDirChooser ?>
|
||||
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
|
||||
<CustomAction Id="JpCheckInstallDir" BinaryKey="JpCaDll" DllEntry="CheckInstallDir" />
|
||||
<?endif?>
|
||||
|
||||
<UI>
|
||||
<?ifdef JpInstallDirChooser ?>
|
||||
<Dialog Id="JpInvalidInstallDir" Width="300" Height="85" Title="[ProductName] Setup" NoMinimize="yes">
|
||||
<Control Id="JpInvalidInstallDirYes" Type="PushButton" X="100" Y="55" Width="50" Height="15" Default="no" Cancel="no" Text="Yes">
|
||||
<Publish Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
|
||||
</Control>
|
||||
<Control Id="JpInvalidInstallDirNo" Type="PushButton" X="150" Y="55" Width="50" Height="15" Default="yes" Cancel="yes" Text="No">
|
||||
<Publish Event="NewDialog" Value="InstallDirDlg">1</Publish>
|
||||
</Control>
|
||||
<Control Id="Text" Type="Text" X="25" Y="15" Width="250" Height="30" TabSkip="no">
|
||||
<Text>!(loc.message.install.dir.exist)</Text>
|
||||
</Control>
|
||||
</Dialog>
|
||||
|
||||
<!--
|
||||
Run WixUI_InstallDir dialog in the default install directory.
|
||||
-->
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR"/>
|
||||
<UIRef Id="WixUI_InstallDir" />
|
||||
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="JpCheckInstallDir" Order="3">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="JpInvalidInstallDir" Order="5">INSTALLDIR_VALID="0"</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="5">INSTALLDIR_VALID="1"</Publish>
|
||||
|
||||
<?ifndef JpLicenseRtf ?>
|
||||
<!--
|
||||
No license file provided.
|
||||
Override the dialog sequence in built-in dialog set "WixUI_InstallDir"
|
||||
to exclude license dialog.
|
||||
-->
|
||||
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">1</Publish>
|
||||
<?endif?>
|
||||
|
||||
<?else?>
|
||||
|
||||
<?ifdef JpLicenseRtf ?>
|
||||
<UIRef Id="WixUI_Minimal" />
|
||||
<?endif?>
|
||||
|
||||
<?endif?>
|
||||
</UI>
|
||||
|
||||
<?ifdef JpLicenseRtf ?>
|
||||
<WixVariable Id="WixUILicenseRtf" Value="$(var.JpLicenseRtf)"/>
|
||||
<?endif?>
|
||||
|
||||
</Product>
|
||||
</Wix>
|
||||
@@ -0,0 +1,3 @@
|
||||
mime-type=application/psbt
|
||||
extension=psbt
|
||||
description=Partially Signed Bitcoin Transaction
|
||||
@@ -0,0 +1,3 @@
|
||||
mime-type=application/bitcoin-transaction
|
||||
extension=txn
|
||||
description=Bitcoin Transaction
|
||||
@@ -12,7 +12,7 @@ public class AboutController {
|
||||
private Label title;
|
||||
|
||||
public void initializeView() {
|
||||
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION);
|
||||
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION + MainApp.APP_VERSION_SUFFIX);
|
||||
}
|
||||
|
||||
public void setStage(Stage stage) {
|
||||
|
||||
@@ -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;
|
||||
@@ -18,6 +15,7 @@ import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
@@ -87,6 +85,9 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private Menu savePSBT;
|
||||
|
||||
@FXML
|
||||
private MenuItem savePSBTBinary;
|
||||
|
||||
@FXML
|
||||
private MenuItem exportWallet;
|
||||
|
||||
@@ -120,6 +121,12 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private CheckMenuItem useHdCameraResolution;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem showLoadingLog;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem showUtxosChart;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem showTxHex;
|
||||
|
||||
@@ -129,6 +136,9 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem refreshWallet;
|
||||
|
||||
@FXML
|
||||
private MenuItem sendToMany;
|
||||
|
||||
@FXML
|
||||
private StackPane rootStack;
|
||||
|
||||
@@ -167,11 +177,7 @@ public class AppController implements Initializable {
|
||||
boolean success = false;
|
||||
if(db.hasFiles()) {
|
||||
for(File file : db.getFiles()) {
|
||||
if(isWalletFile(file)) {
|
||||
openWalletFile(file, true);
|
||||
} else {
|
||||
openTransactionFile(file);
|
||||
}
|
||||
openFile(file);
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
@@ -227,6 +233,8 @@ public class AppController implements Initializable {
|
||||
if(tabs.getTabs().isEmpty()) {
|
||||
Stage tabStage = (Stage)tabs.getScene().getWindow();
|
||||
tabStage.setTitle("Sparrow");
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -259,9 +267,14 @@ public class AppController implements Initializable {
|
||||
hideEmptyUsedAddresses.setSelected(Config.get().isHideEmptyUsedAddresses());
|
||||
useHdCameraResolution.setSelected(Config.get().isHdCapture());
|
||||
showTxHex.setSelected(Config.get().isShowTransactionHex());
|
||||
showLoadingLog.setSelected(Config.get().isShowLoadingLog());
|
||||
showUtxosChart.setSelected(Config.get().isShowUtxosChart());
|
||||
saveTransaction.setDisable(true);
|
||||
savePSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
|
||||
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
|
||||
exportWallet.setDisable(true);
|
||||
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
|
||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||
|
||||
setServerType(Config.get().getServerType());
|
||||
serverToggle.setSelected(isConnected());
|
||||
@@ -311,7 +324,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void showDocumentation(ActionEvent event) throws IOException {
|
||||
public void showDocumentation(ActionEvent event) {
|
||||
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/docs");
|
||||
}
|
||||
|
||||
@@ -324,7 +337,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void submitBugReport(ActionEvent event) throws IOException {
|
||||
public void submitBugReport(ActionEvent event) {
|
||||
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/submitbugreport");
|
||||
}
|
||||
|
||||
@@ -343,7 +356,9 @@ public class AppController implements Initializable {
|
||||
stage.setTitle("About " + MainApp.APP_NAME);
|
||||
stage.initStyle(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.OSX ? StageStyle.UNDECORATED : StageStyle.DECORATED);
|
||||
stage.setResizable(false);
|
||||
stage.setScene(new Scene(root));
|
||||
Scene scene = new Scene(root);
|
||||
AppServices.onEscapePressed(scene, stage::close);
|
||||
stage.setScene(scene);
|
||||
controller.setStage(stage);
|
||||
controller.initializeView();
|
||||
setStageIcon(stage);
|
||||
@@ -432,7 +447,7 @@ public class AppController implements Initializable {
|
||||
showErrorDialog("Invalid PSBT", e.getMessage());
|
||||
} catch(TransactionParseException e) {
|
||||
showErrorDialog("Invalid transaction", e.getMessage());
|
||||
} catch(ParseException e) {
|
||||
} catch(Exception e) {
|
||||
showErrorDialog("Invalid file", e.getMessage());
|
||||
}
|
||||
}
|
||||
@@ -664,6 +679,18 @@ public class AppController implements Initializable {
|
||||
Config.get().setHdCapture(item.isSelected());
|
||||
}
|
||||
|
||||
public void showLoadingLog(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setShowLoadingLog(item.isSelected());
|
||||
EventManager.get().post(new LoadingLogChangedEvent(item.isSelected()));
|
||||
}
|
||||
|
||||
public void showUtxosChart(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setShowUtxosChart(item.isSelected());
|
||||
EventManager.get().post(new UtxosChartChangedEvent(item.isSelected()));
|
||||
}
|
||||
|
||||
public void showTxHex(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setShowTransactionHex(item.isSelected());
|
||||
@@ -677,9 +704,12 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new BitcoinUnitChangedEvent(unit));
|
||||
}
|
||||
|
||||
private boolean isWalletFile(File file) {
|
||||
FileType fileType = IOUtils.getFileType(file);
|
||||
return FileType.JSON.equals(fileType) || FileType.BINARY.equals(fileType);
|
||||
public void openFile(File file) {
|
||||
if(isWalletFile(file)) {
|
||||
openWalletFile(file, true);
|
||||
} else {
|
||||
openTransactionFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
private void setServerToggleTooltip(Integer currentBlockHeight) {
|
||||
@@ -729,17 +759,11 @@ public class AppController implements Initializable {
|
||||
public void openWalletFile(File file, boolean forceSameWindow) {
|
||||
try {
|
||||
Storage storage = new Storage(file);
|
||||
FileType fileType = IOUtils.getFileType(file);
|
||||
if(FileType.JSON.equals(fileType)) {
|
||||
Storage.WalletBackupAndKey walletBackupAndKey = storage.loadWallet();
|
||||
checkWalletNetwork(walletBackupAndKey.wallet);
|
||||
restorePublicKeysFromSeed(walletBackupAndKey.wallet, null);
|
||||
if(!walletBackupAndKey.wallet.isValid()) {
|
||||
throw new IllegalStateException("Wallet file is not valid.");
|
||||
}
|
||||
addWalletTabOrWindow(storage, walletBackupAndKey.wallet, walletBackupAndKey.backupWallet, forceSameWindow);
|
||||
} else if(FileType.BINARY.equals(fileType)) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
if(!storage.isEncrypted()) {
|
||||
WalletBackupAndKey walletBackupAndKey = storage.loadUnencryptedWallet();
|
||||
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
|
||||
} else {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(storage.getWalletName(null), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> optionalPassword = dlg.showAndWait();
|
||||
if(optionalPassword.isEmpty()) {
|
||||
return;
|
||||
@@ -748,20 +772,12 @@ public class AppController implements Initializable {
|
||||
SecureString password = optionalPassword.get();
|
||||
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage, password);
|
||||
loadWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
|
||||
Storage.WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
|
||||
try {
|
||||
checkWalletNetwork(walletBackupAndKey.wallet);
|
||||
restorePublicKeysFromSeed(walletBackupAndKey.wallet, walletBackupAndKey.key);
|
||||
addWalletTabOrWindow(storage, walletBackupAndKey.wallet, walletBackupAndKey.backupWallet, forceSameWindow);
|
||||
} catch(Exception e) {
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||
} finally {
|
||||
walletBackupAndKey.key.clear();
|
||||
}
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.END, "Done"));
|
||||
WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
|
||||
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
|
||||
});
|
||||
loadWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.END, "Failed"));
|
||||
Throwable exception = loadWalletService.getException();
|
||||
if(exception instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
@@ -776,19 +792,42 @@ public class AppController implements Initializable {
|
||||
password.clear();
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
loadWalletService.start();
|
||||
} else {
|
||||
throw new IOException("Unsupported file type");
|
||||
}
|
||||
} 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void openWallet(Storage storage, WalletBackupAndKey walletBackupAndKey, AppController appController, boolean forceSameWindow) {
|
||||
try {
|
||||
checkWalletNetwork(walletBackupAndKey.getWallet());
|
||||
restorePublicKeysFromSeed(walletBackupAndKey.getWallet(), walletBackupAndKey.getKey());
|
||||
if(!walletBackupAndKey.getWallet().isValid()) {
|
||||
throw new IllegalStateException("Wallet file is not valid.");
|
||||
}
|
||||
AppController walletAppController = appController.addWalletTabOrWindow(storage, walletBackupAndKey.getWallet(), walletBackupAndKey.getBackupWallet(), forceSameWindow);
|
||||
for(Map.Entry<Storage, WalletBackupAndKey> entry : walletBackupAndKey.getChildWallets().entrySet()) {
|
||||
openWallet(entry.getKey(), entry.getValue(), walletAppController, true);
|
||||
}
|
||||
Platform.runLater(() -> selectTab(walletBackupAndKey.getWallet()));
|
||||
} catch(Exception e) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||
} finally {
|
||||
walletBackupAndKey.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkWalletNetwork(Wallet wallet) {
|
||||
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.get()) {
|
||||
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.get() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
|
||||
@@ -857,7 +896,9 @@ public class AppController implements Initializable {
|
||||
new Electrum(),
|
||||
new SpecterDesktop(),
|
||||
new CoboVaultSinglesig(), new CoboVaultMultisig(),
|
||||
new PassportSinglesig());
|
||||
new PassportSinglesig(),
|
||||
new KeystoneSinglesig(), new KeystoneMultisig(),
|
||||
new CaravanMultisig());
|
||||
for(WalletImport importer : walletImporters) {
|
||||
try(FileInputStream inputStream = new FileInputStream(file)) {
|
||||
if(importer.isEncrypted(file) && password == null) {
|
||||
@@ -921,17 +962,28 @@ public class AppController implements Initializable {
|
||||
walletFile.delete();
|
||||
}
|
||||
|
||||
if(wallet.isEncrypted()) {
|
||||
throw new IllegalArgumentException("Imported wallet must be unencrypted");
|
||||
}
|
||||
|
||||
Storage storage = new Storage(Storage.getWalletFile(wallet.getName()));
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.UPDATE_NEW);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
if(password.get().length() == 0) {
|
||||
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||
addWalletTabOrWindow(storage, wallet, null, false);
|
||||
try {
|
||||
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||
storage.saveWallet(wallet);
|
||||
checkWalletNetwork(wallet);
|
||||
restorePublicKeysFromSeed(wallet, null);
|
||||
addWalletTabOrWindow(storage, wallet, null, false);
|
||||
} catch(IOException | StorageException | MnemonicException e) {
|
||||
log.error("Error saving imported wallet", e);
|
||||
}
|
||||
} else {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()), TimedEvent.Action.END, "Done"));
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = null;
|
||||
|
||||
@@ -940,7 +992,12 @@ public class AppController implements Initializable {
|
||||
key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
wallet.encrypt(key);
|
||||
storage.setEncryptionPubKey(encryptionPubKey);
|
||||
storage.saveWallet(wallet);
|
||||
checkWalletNetwork(wallet);
|
||||
restorePublicKeysFromSeed(wallet, key);
|
||||
addWalletTabOrWindow(storage, wallet, null, false);
|
||||
} catch(IOException | StorageException | MnemonicException e) {
|
||||
log.error("Error saving imported wallet", e);
|
||||
} finally {
|
||||
encryptionFullKey.clear();
|
||||
if(key != null) {
|
||||
@@ -949,10 +1006,10 @@ public class AppController implements Initializable {
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()), TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Failed"));
|
||||
showErrorDialog("Error encrypting wallet", keyDerivationService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()), TimedEvent.Action.START, "Encrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.START, "Encrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
}
|
||||
@@ -997,6 +1054,28 @@ public class AppController implements Initializable {
|
||||
messageSignDialog.showAndWait();
|
||||
}
|
||||
|
||||
public void sendToMany(ActionEvent event) {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.WALLET) {
|
||||
WalletTabData walletTabData = (WalletTabData) tabData;
|
||||
Wallet wallet = walletTabData.getWallet();
|
||||
BitcoinUnit bitcoinUnit = Config.get().getBitcoinUnit();
|
||||
if(bitcoinUnit == BitcoinUnit.AUTO) {
|
||||
bitcoinUnit = wallet.getAutoUnit();
|
||||
}
|
||||
|
||||
SendToManyDialog sendToManyDialog = new SendToManyDialog(bitcoinUnit);
|
||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||
optPayments.ifPresent(payments -> {
|
||||
if(!payments.isEmpty()) {
|
||||
EventManager.get().post(new SendActionEvent(wallet, new ArrayList<>(wallet.getWalletUtxos().keySet())));
|
||||
Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(wallet, payments)));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void minimizeToTray(ActionEvent event) {
|
||||
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
|
||||
}
|
||||
@@ -1011,18 +1090,18 @@ public class AppController implements Initializable {
|
||||
walletTabData.getStorage().backupTempWallet();
|
||||
wallet.clearHistory();
|
||||
AppServices.clearTransactionHistoryCache(wallet);
|
||||
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, walletTabData.getStorage().getWalletFile()));
|
||||
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, walletTabData.getWalletForm().getWalletId()));
|
||||
}
|
||||
}
|
||||
|
||||
public void addWalletTabOrWindow(Storage storage, Wallet wallet, Wallet backupWallet, boolean forceSameWindow) {
|
||||
Window existingWalletWindow = AppServices.get().getWindowForWallet(storage);
|
||||
public AppController addWalletTabOrWindow(Storage storage, Wallet wallet, Wallet backupWallet, boolean forceSameWindow) {
|
||||
Window existingWalletWindow = AppServices.get().getWindowForWallet(storage.getWalletId(wallet));
|
||||
if(existingWalletWindow instanceof Stage) {
|
||||
Stage existingWalletStage = (Stage)existingWalletWindow;
|
||||
existingWalletStage.toFront();
|
||||
|
||||
EventManager.get().post(new ViewWalletEvent(existingWalletWindow, wallet, storage));
|
||||
return;
|
||||
return this;
|
||||
}
|
||||
|
||||
if(!forceSameWindow && Config.get().isOpenWalletsInNewWindows() && !getOpenWallets().isEmpty()) {
|
||||
@@ -1031,17 +1110,16 @@ public class AppController implements Initializable {
|
||||
stage.toFront();
|
||||
stage.setX(AppServices.get().getWalletWindowMaxX() + 30);
|
||||
appController.addWalletTab(storage, wallet, backupWallet);
|
||||
return appController;
|
||||
} else {
|
||||
addWalletTab(storage, wallet, backupWallet);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public void addWalletTab(Storage storage, Wallet wallet, Wallet backupWallet) {
|
||||
try {
|
||||
String name = storage.getWalletFile().getName();
|
||||
if(name.endsWith(".json")) {
|
||||
name = name.substring(0, name.lastIndexOf('.'));
|
||||
}
|
||||
String name = storage.getWalletName(wallet);
|
||||
if(!name.equals(wallet.getName())) {
|
||||
wallet.setName(name);
|
||||
}
|
||||
@@ -1106,7 +1184,8 @@ public class AppController implements Initializable {
|
||||
|
||||
private void addTransactionTab(String name, File file, byte[] bytes) throws PSBTParseException, ParseException, TransactionParseException {
|
||||
if(PSBT.isPSBT(bytes)) {
|
||||
PSBT psbt = new PSBT(bytes);
|
||||
//Don't verify signatures here - provided PSBT may omit UTXO data that can be found when combining with an existing PSBT
|
||||
PSBT psbt = new PSBT(bytes, false);
|
||||
addTransactionTab(name, file, psbt);
|
||||
} else if(Transaction.isTransaction(bytes)) {
|
||||
try {
|
||||
@@ -1182,6 +1261,16 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
if(psbt != null) {
|
||||
try {
|
||||
//Any PSBTs that have reached this point could not be combined with an existing PSBT. Verify signatures before continuing
|
||||
psbt.verifySignatures();
|
||||
} catch(PSBTSignatureException e) {
|
||||
AppServices.showErrorDialog("Invalid PSBT", e.getMessage());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
String tabName = name;
|
||||
|
||||
@@ -1277,6 +1366,28 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new ThemeChangedEvent(selectedTheme));
|
||||
}
|
||||
|
||||
private void serverToggleStartAnimation() {
|
||||
Node thumbArea = serverToggle.lookup(".thumb-area");
|
||||
if(thumbArea != null) {
|
||||
FadeTransition fadeTransition = new FadeTransition(Duration.millis(1000), thumbArea);
|
||||
fadeTransition.setFromValue(1.0);
|
||||
fadeTransition.setToValue(0.4);
|
||||
fadeTransition.setAutoReverse(true);
|
||||
fadeTransition.setCycleCount(Animation.INDEFINITE);
|
||||
fadeTransition.play();
|
||||
serverToggle.setUserData(fadeTransition);
|
||||
}
|
||||
}
|
||||
|
||||
private void serverToggleStopAnimation() {
|
||||
if(serverToggle.getUserData() != null) {
|
||||
FadeTransition fadeTransition = (FadeTransition)serverToggle.getUserData();
|
||||
fadeTransition.stop();
|
||||
fadeTransition.getNode().setOpacity(1.0);
|
||||
serverToggle.setUserData(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void tabLabelStartAnimation(Wallet wallet) {
|
||||
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((WalletTabData)tab.getUserData()).getWallet() == wallet).forEach(this::tabLabelStartAnimation);
|
||||
}
|
||||
@@ -1364,6 +1475,8 @@ public class AppController implements Initializable {
|
||||
saveTransaction.setVisible(false);
|
||||
}
|
||||
exportWallet.setDisable(true);
|
||||
showLoadingLog.setDisable(true);
|
||||
showUtxosChart.setDisable(true);
|
||||
showTxHex.setDisable(false);
|
||||
} else if(event instanceof WalletTabSelectedEvent) {
|
||||
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
|
||||
@@ -1371,6 +1484,8 @@ public class AppController implements Initializable {
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(true);
|
||||
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
|
||||
showLoadingLog.setDisable(false);
|
||||
showUtxosChart.setDisable(false);
|
||||
showTxHex.setDisable(true);
|
||||
}
|
||||
}
|
||||
@@ -1396,7 +1511,7 @@ public class AppController implements Initializable {
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
WalletTabData walletTabData = (WalletTabData)tabData;
|
||||
if(walletTabData.getWalletForm().getWalletFile().equals(event.getWalletFile())) {
|
||||
if(walletTabData.getWalletForm().getWalletId().equals(event.getWalletId())) {
|
||||
exportWallet.setDisable(!event.getWallet().isValid());
|
||||
}
|
||||
}
|
||||
@@ -1544,12 +1659,14 @@ public class AppController implements Initializable {
|
||||
if(!statusBar.getText().contains(TRYING_ANOTHER_SERVER_MESSAGE)) {
|
||||
statusUpdated(new StatusEvent(event.getStatus(), 120));
|
||||
}
|
||||
serverToggleStartAnimation();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void connectionFailed(ConnectionFailedEvent event) {
|
||||
String status = CONNECTION_FAILED_PREFIX + event.getMessage();
|
||||
statusUpdated(new StatusEvent(status));
|
||||
serverToggleStopAnimation();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1557,6 +1674,7 @@ public class AppController implements Initializable {
|
||||
String status = "Connected to " + Config.get().getServerAddress() + " at height " + event.getBlockHeight();
|
||||
statusUpdated(new StatusEvent(status));
|
||||
setServerToggleTooltip(event.getBlockHeight());
|
||||
serverToggleStopAnimation();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1571,6 +1689,7 @@ public class AppController implements Initializable {
|
||||
for(Wallet wallet : getOpenWallets().keySet()) {
|
||||
tabLabelStopAnimation(wallet);
|
||||
}
|
||||
serverToggleStopAnimation();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1707,6 +1826,17 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void viewWallet(ViewWalletEvent event) {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData) tab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.WALLET) {
|
||||
WalletTabData walletTabData = (WalletTabData) tabData;
|
||||
if(event.getStorage().getWalletId(event.getWallet()).equals(walletTabData.getWalletForm().getWalletId())) {
|
||||
tabs.getSelectionModel().select(tab);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData) tab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.WALLET) {
|
||||
@@ -1758,14 +1888,22 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void requestWalletOpen(RequestWalletOpenEvent event) {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
openWallet(true);
|
||||
if(event.getFile() != null) {
|
||||
openWalletFile(event.getFile(), true);
|
||||
} else {
|
||||
openWallet(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void requestTransactionOpen(RequestTransactionOpenEvent event) {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
openTransactionFromFile(null);
|
||||
if(event.getFile() != null) {
|
||||
openTransactionFile(event.getFile());
|
||||
} else {
|
||||
openTransactionFromFile(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1775,4 +1913,14 @@ public class AppController implements Initializable {
|
||||
openTransactionFromQR(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void sendAction(SendActionEvent event) {
|
||||
selectTab(event.getWallet());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void recieveAction(ReceiveActionEvent event) {
|
||||
selectTab(event.getWallet());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,16 @@ import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@@ -27,28 +26,35 @@ import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
import org.berndpruenster.netlayer.tor.Tor;
|
||||
import org.controlsfx.control.HyperlinkLabel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.desktop.OpenFilesHandler;
|
||||
import java.awt.desktop.OpenURIHandler;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.net.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AppServices {
|
||||
@@ -61,6 +67,7 @@ public class AppServices {
|
||||
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
|
||||
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
|
||||
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
|
||||
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
||||
|
||||
private static AppServices INSTANCE;
|
||||
|
||||
@@ -94,6 +101,10 @@ public class AppServices {
|
||||
|
||||
private static List<Device> devices;
|
||||
|
||||
private static final List<File> argFiles = new ArrayList<>();
|
||||
|
||||
private static final List<URI> argUris = new ArrayList<>();
|
||||
|
||||
private static final Map<Address, BitcoinURI> payjoinURIs = new HashMap<>();
|
||||
|
||||
private final ChangeListener<Boolean> onlineServicesListener = new ChangeListener<>() {
|
||||
@@ -113,6 +124,14 @@ public class AppServices {
|
||||
}
|
||||
};
|
||||
|
||||
private static final OpenURIHandler openURIHandler = event -> {
|
||||
openURI(event.getURI());
|
||||
};
|
||||
|
||||
private static final OpenFilesHandler openFilesHandler = event -> {
|
||||
openFiles(event.getFiles(), null);
|
||||
};
|
||||
|
||||
public AppServices(MainApp application) {
|
||||
this.application = application;
|
||||
EventManager.get().register(this);
|
||||
@@ -134,6 +153,8 @@ public class AppServices {
|
||||
restartServices();
|
||||
}
|
||||
}
|
||||
|
||||
addURIHandlers();
|
||||
}
|
||||
|
||||
private void restartServices() {
|
||||
@@ -219,7 +240,7 @@ public class AppServices {
|
||||
connectionService.setRestartOnFailure(false);
|
||||
if(tlsServerException.getCause().getMessage().contains("PKIX path building failed")) {
|
||||
File crtFile = Config.get().getElectrumServerCert();
|
||||
if(crtFile != null) {
|
||||
if(crtFile != null && Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
|
||||
AppServices.showErrorDialog("SSL Handshake Failed", "The configured server certificate at " + crtFile.getAbsolutePath() + " did not match the certificate provided by the server at " + tlsServerException.getServer().getHost() + "." +
|
||||
"\n\nThis may indicate a man-in-the-middle attack!" +
|
||||
"\n\nChange the configured server certificate if you would like to proceed.");
|
||||
@@ -352,18 +373,36 @@ public class AppServices {
|
||||
return Tor.getDefault() != null;
|
||||
}
|
||||
|
||||
public static boolean isUsingProxy() {
|
||||
return isTorRunning() || Config.get().isUseProxy();
|
||||
}
|
||||
|
||||
public static Proxy getProxy() {
|
||||
return getProxy(TOR_DEFAULT_PROXY_CIRCUIT_ID);
|
||||
}
|
||||
|
||||
public static Proxy getProxy(String proxyCircuitId) {
|
||||
Config config = Config.get();
|
||||
Proxy proxy = null;
|
||||
if(config.isUseProxy()) {
|
||||
HostAndPort proxy = HostAndPort.fromString(config.getProxyServer());
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress(proxy.getHost(), proxy.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
||||
return new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
HostAndPort proxyHostAndPort = HostAndPort.fromString(config.getProxyServer());
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
||||
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
} else if(AppServices.isTorRunning()) {
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress("localhost", TorService.PROXY_PORT);
|
||||
return new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
}
|
||||
|
||||
return null;
|
||||
//Setting new proxy authentication credentials will force a new Tor circuit to be created
|
||||
if(proxy != null) {
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
public PasswordAuthentication getPasswordAuthentication() {
|
||||
return (new PasswordAuthentication("user", proxyCircuitId.toCharArray()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
static void initialize(MainApp application) {
|
||||
@@ -415,6 +454,14 @@ public class AppServices {
|
||||
stage.hide();
|
||||
}
|
||||
|
||||
public static void onEscapePressed(Scene scene, Runnable runnable) {
|
||||
scene.setOnKeyPressed(event -> {
|
||||
if(event.getCode() == KeyCode.ESCAPE) {
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Map<Wallet, Storage> getOpenWallets() {
|
||||
Map<Wallet, Storage> openWallets = new LinkedHashMap<>();
|
||||
for(List<WalletTabData> walletTabDataList : walletWindows.values()) {
|
||||
@@ -426,8 +473,8 @@ public class AppServices {
|
||||
return openWallets;
|
||||
}
|
||||
|
||||
public Window getWindowForWallet(Storage storage) {
|
||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getStorage().getWalletFile().equals(storage.getWalletFile()))).map(Map.Entry::getKey).findFirst();
|
||||
public Window getWindowForWallet(String walletId) {
|
||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getWalletForm().getWalletId().equals(walletId))).map(Map.Entry::getKey).findFirst();
|
||||
return optWindow.orElse(null);
|
||||
}
|
||||
|
||||
@@ -500,6 +547,10 @@ public class AppServices {
|
||||
ElectrumServer.clearRetrievedScriptHashes(wallet);
|
||||
}
|
||||
|
||||
public static boolean isWalletFile(File file) {
|
||||
return Storage.isWalletFile(file);
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
|
||||
return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons);
|
||||
}
|
||||
@@ -514,6 +565,29 @@ public class AppServices {
|
||||
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(title);
|
||||
|
||||
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
|
||||
Matcher matcher = linkPattern.matcher(content);
|
||||
if(matcher.find()) {
|
||||
String link = matcher.group(1);
|
||||
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(content);
|
||||
hyperlinkLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
hyperlinkLabel.setMaxHeight(Double.MAX_VALUE);
|
||||
hyperlinkLabel.getStyleClass().add("content");
|
||||
Label label = new Label();
|
||||
hyperlinkLabel.setPrefWidth(Math.max(360, TextUtils.computeTextWidth(label.getFont(), link, 0.0D) + 50));
|
||||
hyperlinkLabel.setOnAction(event -> {
|
||||
alert.close();
|
||||
get().getApplication().getHostServices().showDocument(link);
|
||||
});
|
||||
alert.getDialogPane().setContent(hyperlinkLabel);
|
||||
}
|
||||
|
||||
String[] lines = content.split("\r\n|\r|\n");
|
||||
if(lines.length > 3) {
|
||||
alert.getDialogPane().setPrefHeight(200 + lines.length * 20);
|
||||
}
|
||||
|
||||
moveToActiveWindowScreen(alert);
|
||||
return alert.showAndWait();
|
||||
}
|
||||
@@ -545,10 +619,6 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
public void moveToWalletWindowScreen(Storage storage, Dialog<?> dialog) {
|
||||
moveToWindowScreen(getWindowForWallet(storage), dialog);
|
||||
}
|
||||
|
||||
public static void moveToWindowScreen(Window currentWindow, Dialog<?> dialog) {
|
||||
Window newWindow = dialog.getDialogPane().getScene().getWindow();
|
||||
DialogPane dialogPane = dialog.getDialogPane();
|
||||
@@ -567,6 +637,135 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
static void parseFileUriArguments(List<String> fileUriArguments) {
|
||||
for(String fileUri : fileUriArguments) {
|
||||
try {
|
||||
File file = new File(fileUri.replace("~", System.getProperty("user.home")));
|
||||
if(file.exists()) {
|
||||
argFiles.add(file);
|
||||
continue;
|
||||
}
|
||||
URI uri = new URI(fileUri);
|
||||
argUris.add(uri);
|
||||
} catch(URISyntaxException e) {
|
||||
log.warn("Could not parse " + fileUri + " as a valid file or URI");
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void openFileUriArguments(Window window) {
|
||||
openFiles(argFiles, window);
|
||||
argFiles.clear();
|
||||
|
||||
for(URI argUri : argUris) {
|
||||
openURI(argUri);
|
||||
}
|
||||
argUris.clear();
|
||||
}
|
||||
|
||||
private static void openFiles(List<File> files, Window window) {
|
||||
final List<File> openFiles = new ArrayList<>(files);
|
||||
Platform.runLater(() -> {
|
||||
Window openWindow = window;
|
||||
if(openWindow == null) {
|
||||
openWindow = getActiveWindow();
|
||||
}
|
||||
|
||||
if(openWindow instanceof Stage) {
|
||||
((Stage)openWindow).setAlwaysOnTop(true);
|
||||
((Stage)openWindow).setAlwaysOnTop(false);
|
||||
}
|
||||
|
||||
for(File file : openFiles) {
|
||||
if(isWalletFile(file)) {
|
||||
EventManager.get().post(new RequestWalletOpenEvent(openWindow, file));
|
||||
} else {
|
||||
EventManager.get().post(new RequestTransactionOpenEvent(openWindow, file));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void openURI(URI uri) {
|
||||
Platform.runLater(() -> {
|
||||
if("bitcoin".equals(uri.getScheme())) {
|
||||
openBitcoinUri(uri);
|
||||
} else if("aopp".equals(uri.getScheme())) {
|
||||
openAddressOwnershipProof(uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void addURIHandlers() {
|
||||
try {
|
||||
if(Desktop.isDesktopSupported()) {
|
||||
if(Desktop.getDesktop().isSupported(Desktop.Action.APP_OPEN_FILE)) {
|
||||
Desktop.getDesktop().setOpenFileHandler(openFilesHandler);
|
||||
}
|
||||
if(Desktop.getDesktop().isSupported(Desktop.Action.APP_OPEN_URI)) {
|
||||
Desktop.getDesktop().setOpenURIHandler(openURIHandler);
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Could not add URI handler", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void openBitcoinUri(URI uri) {
|
||||
try {
|
||||
BitcoinURI bitcoinURI = new BitcoinURI(uri.toString());
|
||||
Wallet wallet = selectWallet(null, "pay from");
|
||||
|
||||
if(wallet != null) {
|
||||
final Wallet sendingWallet = wallet;
|
||||
EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getWalletUtxos().keySet())));
|
||||
Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment()))));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
showErrorDialog("Not a valid bitcoin URI", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void openAddressOwnershipProof(URI uri) {
|
||||
try {
|
||||
Aopp aopp = new Aopp(uri);
|
||||
Wallet wallet = selectWallet(aopp.getScriptType(), "send proof of address");
|
||||
|
||||
if(wallet != null) {
|
||||
EventManager.get().post(new ReceiveActionEvent(wallet));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveProofEvent(wallet, aopp)));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
showErrorDialog("Not a valid AOPP URI", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Wallet selectWallet(ScriptType scriptType, String actionDescription) {
|
||||
Wallet wallet = null;
|
||||
List<Wallet> wallets = get().getOpenWallets().keySet().stream().filter(w -> scriptType == null || w.getScriptType() == scriptType).collect(Collectors.toList());
|
||||
if(wallets.isEmpty()) {
|
||||
showErrorDialog("No wallet available", "Open a" + (scriptType == null ? "" : " " + scriptType.getDescription()) + " wallet to " + actionDescription + ".");
|
||||
} else if(wallets.size() == 1) {
|
||||
wallet = wallets.iterator().next();
|
||||
} else {
|
||||
ChoiceDialog<Wallet> walletChoiceDialog = new ChoiceDialog<>(wallets.iterator().next(), wallets);
|
||||
walletChoiceDialog.setTitle("Choose Wallet");
|
||||
walletChoiceDialog.setHeaderText("Choose a wallet to " + actionDescription);
|
||||
Image image = new Image("/image/sparrow-small.png");
|
||||
walletChoiceDialog.getDialogPane().setGraphic(new ImageView(image));
|
||||
setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow());
|
||||
moveToActiveWindowScreen(walletChoiceDialog);
|
||||
Optional<Wallet> optWallet = walletChoiceDialog.showAndWait();
|
||||
if(optWallet.isPresent()) {
|
||||
wallet = optWallet.get();
|
||||
}
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public static Font getMonospaceFont() {
|
||||
return Font.font("Roboto Mono", 13);
|
||||
}
|
||||
@@ -637,7 +836,10 @@ public class AppServices {
|
||||
@Subscribe
|
||||
public void openWallets(OpenWalletsEvent event) {
|
||||
if(event.getWalletTabDataList().isEmpty()) {
|
||||
walletWindows.remove(event.getWindow());
|
||||
List<WalletTabData> closedTabData = walletWindows.remove(event.getWindow());
|
||||
if(closedTabData != null && !closedTabData.isEmpty()) {
|
||||
EventManager.get().post(new WalletTabsClosedEvent(closedTabData));
|
||||
}
|
||||
} else {
|
||||
walletWindows.put(event.getWindow(), event.getWalletTabDataList());
|
||||
}
|
||||
@@ -646,7 +848,7 @@ public class AppServices {
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if(!Window.getWindows().isEmpty()) {
|
||||
List<File> walletFiles = allWallets.stream().map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList());
|
||||
List<File> walletFiles = allWallets.stream().filter(walletTabData -> walletTabData.getWallet().getMasterWallet() == null).map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList());
|
||||
Config.get().setRecentWalletFiles(Config.get().isLoadRecentWallets() ? walletFiles : Collections.emptyList());
|
||||
}
|
||||
});
|
||||
@@ -663,10 +865,6 @@ public class AppServices {
|
||||
deviceEnumerateService = createDeviceEnumerateService();
|
||||
}
|
||||
|
||||
if(deviceEnumerateService.isRunning()) {
|
||||
deviceEnumerateService.cancel();
|
||||
}
|
||||
|
||||
if(deviceEnumerateService.getState() == Worker.State.CANCELLED) {
|
||||
deviceEnumerateService.reset();
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.FileType;
|
||||
import com.sparrowwallet.sparrow.io.IOUtils;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.Bwt;
|
||||
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.instance.InstanceException;
|
||||
import com.sparrowwallet.sparrow.instance.InstanceList;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Stage;
|
||||
@@ -29,13 +29,17 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MainApp extends Application {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.4.0";
|
||||
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";
|
||||
|
||||
private Stage mainStage;
|
||||
|
||||
private static SparrowInstance sparrowInstance;
|
||||
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e));
|
||||
@@ -94,9 +98,8 @@ public class MainApp extends Application {
|
||||
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null) {
|
||||
//Re-sort to preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(file -> FileType.BINARY.equals(IOUtils.getFileType(file))).collect(Collectors.toList());
|
||||
Collections.reverse(encryptedWalletFiles);
|
||||
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
||||
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
||||
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
||||
sortedWalletFiles.addAll(encryptedWalletFiles);
|
||||
@@ -108,6 +111,8 @@ public class MainApp extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.openFileUriArguments(stage);
|
||||
|
||||
AppServices.get().start();
|
||||
}
|
||||
|
||||
@@ -115,6 +120,9 @@ public class MainApp extends Application {
|
||||
public void stop() throws Exception {
|
||||
AppServices.get().stop();
|
||||
mainStage.close();
|
||||
if(sparrowInstance != null) {
|
||||
sparrowInstance.freeLock();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] argv) {
|
||||
@@ -162,6 +170,19 @@ public class MainApp extends Application {
|
||||
getLogger().info("Using " + Network.get() + " configuration");
|
||||
}
|
||||
|
||||
List<String> fileUriArguments = jCommander.getUnknownOptions();
|
||||
|
||||
try {
|
||||
sparrowInstance = new SparrowInstance(fileUriArguments);
|
||||
sparrowInstance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
||||
} catch(InstanceException e) {
|
||||
getLogger().error("Could not access application lock", e);
|
||||
}
|
||||
|
||||
if(!fileUriArguments.isEmpty()) {
|
||||
AppServices.parseFileUriArguments(fileUriArguments);
|
||||
}
|
||||
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||
SLF4JBridgeHandler.install();
|
||||
com.sun.javafx.application.LauncherImpl.launchApplication(MainApp.class, MainAppPreloader.class, argv);
|
||||
@@ -170,4 +191,31 @@ public class MainApp extends Application {
|
||||
private static Logger getLogger() {
|
||||
return LoggerFactory.getLogger(MainApp.class);
|
||||
}
|
||||
|
||||
private static class SparrowInstance extends InstanceList {
|
||||
private final List<String> fileUriArguments;
|
||||
|
||||
public SparrowInstance(List<String> fileUriArguments) {
|
||||
super(MainApp.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
|
||||
this.fileUriArguments = fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void receiveMessageList(List<String> messageList) {
|
||||
if(messageList != null && !messageList.isEmpty()) {
|
||||
AppServices.parseFileUriArguments(messageList);
|
||||
AppServices.openFileUriArguments(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> sendMessageList() {
|
||||
return fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeExit() {
|
||||
getLogger().info("Opening files/URIs in already running instance, exiting...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ public class WelcomeDialog extends Dialog<Mode> {
|
||||
public WelcomeDialog() {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader welcomeLoader = new FXMLLoader(AppServices.class.getResource("welcome.fxml"));
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
@@ -32,7 +33,7 @@ public class AddressCell extends TreeTableCell<Entry, Entry> {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
Address address = utxoEntry.getAddress();
|
||||
setText(address.toString());
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), null));
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(getTooltipText(utxoEntry));
|
||||
setTooltip(tooltip);
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
public class AddressStringConverter extends StringConverter<Address> {
|
||||
@Override
|
||||
public Address fromString(String value) {
|
||||
// If the specified value is null or zero-length, return null
|
||||
if(value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
value = value.trim();
|
||||
|
||||
if (value.length() < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Address.fromString(value);
|
||||
} catch(InvalidAddressException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Address value) {
|
||||
// If the specified value is null, return a zero-length String
|
||||
if(value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseButton;
|
||||
|
||||
@@ -82,6 +83,10 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rootEntry.getChildren().addListener((ListChangeListener<Entry>) c -> {
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
public void updateAll(NodeEntry rootEntry) {
|
||||
@@ -99,22 +104,21 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
}
|
||||
|
||||
public void updateHistory(List<WalletNode> updatedNodes) {
|
||||
//We only ever add or replace child nodes - never remove in order to keep a full sequence
|
||||
//We only ever add child nodes - never remove in order to keep a full sequence
|
||||
NodeEntry rootEntry = (NodeEntry)getRoot().getValue();
|
||||
|
||||
for(WalletNode updatedNode : updatedNodes) {
|
||||
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
|
||||
|
||||
Optional<Entry> optEntry = rootEntry.getChildren().stream().filter(childEntry -> ((NodeEntry)childEntry).getNode().equals(updatedNode)).findFirst();
|
||||
if(optEntry.isPresent()) {
|
||||
int index = rootEntry.getChildren().indexOf(optEntry.get());
|
||||
rootEntry.getChildren().set(index, nodeEntry);
|
||||
NodeEntry existingEntry = (NodeEntry)optEntry.get();
|
||||
existingEntry.refreshChildren();
|
||||
} else {
|
||||
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
|
||||
rootEntry.getChildren().add(nodeEntry);
|
||||
}
|
||||
}
|
||||
|
||||
sort();
|
||||
refresh();
|
||||
}
|
||||
|
||||
public void updateLabel(Entry entry) {
|
||||
|
||||
@@ -10,16 +10,24 @@ import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.scene.layout.Region;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
public static final DecimalFormat TABLE_BTC_FORMAT = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
|
||||
private final Tooltip tooltip;
|
||||
|
||||
public CoinCell() {
|
||||
super();
|
||||
tooltip = new Tooltip();
|
||||
getStyleClass().add("coin-cell");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
getStyleClass().add("number-field");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -38,7 +46,8 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
BitcoinUnit unit = coinTreeTable.getBitcoinUnit();
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", amount.longValue());
|
||||
final String btcValue = CoinLabel.getBTCFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
DecimalFormat decimalFormat = (amount.longValue() == 0L ? CoinLabel.getBTCFormat() : TABLE_BTC_FORMAT);
|
||||
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setText(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
|
||||
|
||||
@@ -5,8 +5,8 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
@@ -92,7 +92,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
EventManager.get().post(new WalletDataChangedEvent(wallet));
|
||||
//Trigger full wallet rescan
|
||||
wallet.clearHistory();
|
||||
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, storage.getWalletFile()));
|
||||
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
|
||||
}
|
||||
});
|
||||
if(wallet.getBirthDate() == null) {
|
||||
|
||||
@@ -34,7 +34,7 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||
if(entry instanceof UtxoEntry) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
if(utxoEntry.getHashIndex().getHeight() <= 0) {
|
||||
setText("Unconfirmed " + (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)"));
|
||||
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)"));
|
||||
} else {
|
||||
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
|
||||
setText(date);
|
||||
|
||||
@@ -25,8 +25,8 @@ public class DeviceAddressDialog extends DeviceDialog<String> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device) {
|
||||
return new DevicePane(wallet, outputDescriptor, device);
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(wallet, outputDescriptor, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.util.Objects;
|
||||
public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
private final List<String> operationFingerprints;
|
||||
private final Accordion deviceAccordion;
|
||||
private final Button scanButton;
|
||||
private final VBox scanBox;
|
||||
private final Label scanLabel;
|
||||
|
||||
@@ -57,18 +58,19 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
Glyph usb = new Glyph(FontAwesome5Brands.FONT_NAME, FontAwesome5Brands.Glyph.USB);
|
||||
usb.setFontSize(50);
|
||||
scanLabel = new Label("Connect Hardware Wallet");
|
||||
Button button = new Button("Scan...");
|
||||
button.setPrefSize(120, 60);
|
||||
button.setOnAction(event -> {
|
||||
scanButton = new Button("Scan...");
|
||||
scanButton.setPrefSize(120, 60);
|
||||
scanButton.setOnAction(event -> {
|
||||
scan();
|
||||
});
|
||||
scanBox.getChildren().addAll(usb, scanLabel, button);
|
||||
scanBox.getChildren().addAll(usb, scanLabel, scanButton);
|
||||
scanBox.managedProperty().bind(scanBox.visibleProperty());
|
||||
|
||||
stackPane.getChildren().addAll(anchorPane, scanBox);
|
||||
|
||||
List<Device> devices = AppServices.getDevices();
|
||||
if(devices == null || devices.isEmpty()) {
|
||||
scanButton.setDefaultButton(true);
|
||||
scanBox.setVisible(true);
|
||||
} else {
|
||||
Platform.runLater(() -> setDevices(devices));
|
||||
@@ -97,15 +99,21 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
private void scan() {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
scanButton.setText("Scan...");
|
||||
List<Device> devices = enumerateService.getValue();
|
||||
setDevices(devices);
|
||||
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
scanButton.setText("Scan...");
|
||||
deviceAccordion.getPanes().clear();
|
||||
scanButton.setDefaultButton(true);
|
||||
scanBox.setVisible(true);
|
||||
scanLabel.setText(workerStateEvent.getSource().getException().getMessage());
|
||||
});
|
||||
enumerateService.setOnRunning(workerStateEvent -> {
|
||||
scanButton.setText("Scanning...");
|
||||
});
|
||||
enumerateService.start();
|
||||
}
|
||||
|
||||
@@ -122,16 +130,17 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
deviceAccordion.getPanes().clear();
|
||||
|
||||
if(dialogDevices.isEmpty()) {
|
||||
scanButton.setDefaultButton(true);
|
||||
scanBox.setVisible(true);
|
||||
scanLabel.setText("No matching devices found");
|
||||
} else {
|
||||
scanBox.setVisible(false);
|
||||
for(Device device : dialogDevices) {
|
||||
DevicePane devicePane = getDevicePane(device);
|
||||
DevicePane devicePane = getDevicePane(device, dialogDevices.size() == 1);
|
||||
deviceAccordion.getPanes().add(devicePane);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract DevicePane getDevicePane(Device device);
|
||||
protected abstract DevicePane getDevicePane(Device device, boolean defaultDevice);
|
||||
}
|
||||
|
||||
@@ -59,7 +59,9 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
||||
|
||||
public DevicePane(Wallet wallet, Device device) {
|
||||
private boolean defaultDevice;
|
||||
|
||||
public DevicePane(Wallet wallet, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.IMPORT;
|
||||
this.wallet = wallet;
|
||||
@@ -68,6 +70,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
@@ -80,7 +83,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
|
||||
}
|
||||
|
||||
public DevicePane(PSBT psbt, Device device) {
|
||||
public DevicePane(PSBT psbt, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.SIGN;
|
||||
this.wallet = null;
|
||||
@@ -89,6 +92,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
@@ -101,7 +105,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device) {
|
||||
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
|
||||
this.wallet = wallet;
|
||||
@@ -110,6 +114,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
@@ -122,7 +127,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device) {
|
||||
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
|
||||
this.wallet = wallet;
|
||||
@@ -131,6 +136,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.keyDerivation = keyDerivation;
|
||||
this.message = message;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
@@ -145,6 +151,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
private void initialise(Device device) {
|
||||
if(device.isNeedsPinSent()) {
|
||||
unlockButton.setDefaultButton(defaultDevice);
|
||||
unlockButton.setVisible(true);
|
||||
} else if(device.isNeedsPassphraseSent()) {
|
||||
setPassphraseButton.setVisible(true);
|
||||
@@ -229,7 +236,6 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
private void createSignButton() {
|
||||
signButton = new Button("Sign");
|
||||
signButton.setDefaultButton(true);
|
||||
signButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
signButton.setMinWidth(44);
|
||||
signButton.setOnAction(event -> {
|
||||
@@ -242,7 +248,6 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
private void createDisplayAddressButton() {
|
||||
displayAddressButton = new Button("Display Address");
|
||||
displayAddressButton.setDefaultButton(true);
|
||||
displayAddressButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
displayAddressButton.setOnAction(event -> {
|
||||
displayAddressButton.setDisable(true);
|
||||
@@ -259,7 +264,6 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
private void createSignMessageButton() {
|
||||
signMessageButton = new Button("Sign Message");
|
||||
signMessageButton.setDefaultButton(true);
|
||||
signMessageButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
signMessageButton.setOnAction(event -> {
|
||||
signMessageButton.setDisable(true);
|
||||
@@ -284,7 +288,9 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
vBox.setMaxHeight(120);
|
||||
vBox.setSpacing(42);
|
||||
pinField = (CustomPasswordField)TextFields.createClearablePasswordField();
|
||||
Platform.runLater(() -> pinField.requestFocus());
|
||||
enterPinButton = new Button("Enter PIN");
|
||||
enterPinButton.setDefaultButton(true);
|
||||
enterPinButton.setOnAction(event -> {
|
||||
enterPinButton.setDisable(true);
|
||||
sendPin(pinField.getText());
|
||||
@@ -324,9 +330,15 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
CustomPasswordField passphraseField = (CustomPasswordField)TextFields.createClearablePasswordField();
|
||||
passphrase.bind(passphraseField.textProperty());
|
||||
HBox.setHgrow(passphraseField, Priority.ALWAYS);
|
||||
passphraseField.setOnAction(event -> {
|
||||
setExpanded(false);
|
||||
setDescription("Confirm passphrase on device...");
|
||||
sendPassphrase(passphrase.get());
|
||||
});
|
||||
|
||||
SplitMenuButton sendPassphraseButton = new SplitMenuButton();
|
||||
sendPassphraseButton.setText("Send Passphrase");
|
||||
sendPassphraseButton.getStyleClass().add("default-button");
|
||||
sendPassphraseButton.setOnAction(event -> {
|
||||
setExpanded(false);
|
||||
setDescription("Confirm passphrase on device...");
|
||||
@@ -534,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);
|
||||
@@ -604,17 +616,23 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
private void showOperationButton() {
|
||||
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||
if(defaultDevice) {
|
||||
importButton.getStyleClass().add("default-button");
|
||||
}
|
||||
importButton.setVisible(true);
|
||||
showHideLink.setText("Show derivation...");
|
||||
showHideLink.setVisible(true);
|
||||
setContent(getDerivationEntry(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation()));
|
||||
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
|
||||
signButton.setDefaultButton(defaultDevice);
|
||||
signButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.DISPLAY_ADDRESS)) {
|
||||
displayAddressButton.setDefaultButton(defaultDevice);
|
||||
displayAddressButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.SIGN_MESSAGE)) {
|
||||
signMessageButton.setDefaultButton(defaultDevice);
|
||||
signMessageButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
}
|
||||
@@ -627,11 +645,11 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(derivationField, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
|
||||
Button importDerivationButton = new Button("Import");
|
||||
importDerivationButton.setOnAction(event -> {
|
||||
|
||||
@@ -22,8 +22,8 @@ public class DeviceSignDialog extends DeviceDialog<PSBT> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device) {
|
||||
return new DevicePane(psbt, device);
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(psbt, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -26,8 +26,8 @@ public class DeviceSignMessageDialog extends DeviceDialog<String> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device) {
|
||||
return new DevicePane(wallet, message, keyDerivation, device);
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(wallet, message, keyDerivation, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -3,7 +3,9 @@ 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;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@@ -72,9 +74,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
Button viewTransactionButton = new Button("");
|
||||
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
||||
searchGlyph.setFontSize(12);
|
||||
viewTransactionButton.setGraphic(searchGlyph);
|
||||
viewTransactionButton.setGraphic(getViewTransactionGlyph());
|
||||
viewTransactionButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getScene().getWindow(), transactionEntry.getBlockTransaction()));
|
||||
});
|
||||
@@ -83,9 +83,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
Button increaseFeeButton = new Button("");
|
||||
Glyph increaseFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_MEDICAL);
|
||||
increaseFeeGlyph.setFontSize(12);
|
||||
increaseFeeButton.setGraphic(increaseFeeGlyph);
|
||||
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
increaseFeeButton.setOnAction(event -> {
|
||||
increaseFee(transactionEntry);
|
||||
});
|
||||
@@ -94,9 +92,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
if(blockTransaction.getHeight() <= 0 && containsWalletOutputs(transactionEntry)) {
|
||||
Button cpfpButton = new Button("");
|
||||
Glyph cpfpGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SIGN_OUT_ALT);
|
||||
cpfpGlyph.setFontSize(12);
|
||||
cpfpButton.setGraphic(cpfpGlyph);
|
||||
cpfpButton.setGraphic(getIncreaseFeeCPFPGlyph());
|
||||
cpfpButton.setOnAction(event -> {
|
||||
createCpfp(transactionEntry);
|
||||
});
|
||||
@@ -108,7 +104,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||
Address address = nodeEntry.getAddress();
|
||||
setText(address.toString());
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), null));
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(nodeEntry.getNode().getDerivationPath().replace("m", ".."));
|
||||
setTooltip(tooltip);
|
||||
@@ -116,27 +112,21 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
Button receiveButton = new Button("");
|
||||
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
|
||||
receiveGlyph.setFontSize(12);
|
||||
receiveButton.setGraphic(receiveGlyph);
|
||||
receiveButton.setGraphic(getReceiveGlyph());
|
||||
receiveButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
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("");
|
||||
Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY);
|
||||
signMessageGlyph.setFontSize(12);
|
||||
signMessageButton.setGraphic(signMessageGlyph);
|
||||
signMessageButton.setGraphic(getSignMessageGlyph());
|
||||
signMessageButton.setOnAction(event -> {
|
||||
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
|
||||
messageSignDialog.showAndWait();
|
||||
});
|
||||
actionBox.getChildren().add(signMessageButton);
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
@@ -150,9 +140,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
Button viewTransactionButton = new Button("");
|
||||
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
||||
searchGlyph.setFontSize(12);
|
||||
viewTransactionButton.setGraphic(searchGlyph);
|
||||
viewTransactionButton.setGraphic(getViewTransactionGlyph());
|
||||
viewTransactionButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getScene().getWindow(), hashIndexEntry.getBlockTransaction(), hashIndexEntry));
|
||||
});
|
||||
@@ -160,9 +148,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && hashIndexEntry.isSpendable() && !hashIndexEntry.getHashIndex().isSpent()) {
|
||||
Button spendUtxoButton = new Button("");
|
||||
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
|
||||
sendGlyph.setFontSize(12);
|
||||
spendUtxoButton.setGraphic(sendGlyph);
|
||||
spendUtxoButton.setGraphic(getSendGlyph());
|
||||
spendUtxoButton.setOnAction(event -> {
|
||||
sendSelectedUtxos(getTreeTableView(), hashIndexEntry);
|
||||
});
|
||||
@@ -182,6 +168,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
||||
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
||||
.filter(TransactionInput::isReplaceByFeeEnabled)
|
||||
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@@ -249,7 +236,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
|
||||
private static Double getMaxFeeRate() {
|
||||
if(AppServices.getTargetBlockFeeRates().isEmpty()) {
|
||||
if(AppServices.getTargetBlockFeeRates() == null || AppServices.getTargetBlockFeeRates().isEmpty()) {
|
||||
return 100.0;
|
||||
}
|
||||
|
||||
@@ -280,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)
|
||||
@@ -304,31 +296,98 @@ 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() {
|
||||
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
||||
searchGlyph.setFontSize(12);
|
||||
return searchGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getIncreaseFeeRBFGlyph() {
|
||||
Glyph increaseFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_MEDICAL);
|
||||
increaseFeeGlyph.setFontSize(12);
|
||||
return increaseFeeGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getIncreaseFeeCPFPGlyph() {
|
||||
Glyph cpfpGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SIGN_OUT_ALT);
|
||||
cpfpGlyph.setFontSize(12);
|
||||
return cpfpGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getReceiveGlyph() {
|
||||
Glyph receiveGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.ARROW_DOWN);
|
||||
receiveGlyph.setFontSize(12);
|
||||
return receiveGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getSignMessageGlyph() {
|
||||
Glyph signMessageGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PEN_FANCY);
|
||||
signMessageGlyph.setFontSize(12);
|
||||
return signMessageGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getSendGlyph() {
|
||||
Glyph sendGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
|
||||
sendGlyph.setFontSize(12);
|
||||
return sendGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getCopyGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COPY);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getFreezeGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SNOWFLAKE);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getUnfreezeGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SUN);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
||||
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||
copyTxid.setOnAction(AE -> {
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
viewTransaction.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(blockTransaction.getHashAsString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
||||
});
|
||||
|
||||
getItems().add(copyTxid);
|
||||
getItems().add(viewTransaction);
|
||||
|
||||
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
increaseFee.setOnAction(AE -> {
|
||||
hide();
|
||||
increaseFee(transactionEntry);
|
||||
@@ -339,6 +398,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
if(containsWalletOutputs(transactionEntry)) {
|
||||
MenuItem createCpfp = new MenuItem("Increase Effective Fee (CPFP)");
|
||||
createCpfp.setGraphic(getIncreaseFeeCPFPGlyph());
|
||||
createCpfp.setOnAction(AE -> {
|
||||
hide();
|
||||
createCpfp(transactionEntry);
|
||||
@@ -346,11 +406,28 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
getItems().add(createCpfp);
|
||||
}
|
||||
|
||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||
copyTxid.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(blockTransaction.getHashAsString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().add(copyTxid);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionContextMenu extends ContextMenu {
|
||||
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
viewTransaction.setOnAction(AE -> {
|
||||
hide();
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
||||
});
|
||||
|
||||
MenuItem copyDate = new MenuItem("Copy Date");
|
||||
copyDate.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -375,12 +452,32 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(copyDate, copyTxid, copyHeight);
|
||||
getItems().addAll(viewTransaction, copyDate, copyTxid, copyHeight);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AddressContextMenu extends ContextMenu {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
receiveToAddress.setOnAction(event -> {
|
||||
hide();
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
getItems().add(receiveToAddress);
|
||||
|
||||
if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) {
|
||||
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
||||
signVerifyMessage.setGraphic(getSignMessageGlyph());
|
||||
signVerifyMessage.setOnAction(AE -> {
|
||||
hide();
|
||||
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
|
||||
messageSignDialog.showAndWait();
|
||||
});
|
||||
getItems().add(signVerifyMessage);
|
||||
}
|
||||
|
||||
MenuItem copyAddress = new MenuItem("Copy Address");
|
||||
copyAddress.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -406,34 +503,22 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
});
|
||||
|
||||
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
|
||||
|
||||
if(nodeEntry != null) {
|
||||
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
||||
signVerifyMessage.setOnAction(AE -> {
|
||||
hide();
|
||||
MessageSignDialog messageSignDialog = new MessageSignDialog(nodeEntry.getWallet(), nodeEntry.getNode());
|
||||
messageSignDialog.showAndWait();
|
||||
});
|
||||
|
||||
getItems().add(signVerifyMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class HashIndexEntryContextMenu extends ContextMenu {
|
||||
public HashIndexEntryContextMenu(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||
String label = "Copy " + (hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) ? "Transaction Output" : "Transaction Input");
|
||||
MenuItem copyHashIndex = new MenuItem(label);
|
||||
copyHashIndex.setOnAction(AE -> {
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
viewTransaction.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(hashIndexEntry.getHashIndex().toString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), hashIndexEntry.getBlockTransaction()));
|
||||
});
|
||||
getItems().add(copyHashIndex);
|
||||
getItems().add(viewTransaction);
|
||||
|
||||
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && hashIndexEntry.isSpendable() && !hashIndexEntry.getHashIndex().isSpent()) {
|
||||
MenuItem sendSelected = new MenuItem("Send Selected");
|
||||
sendSelected.setGraphic(getSendGlyph());
|
||||
sendSelected.setOnAction(AE -> {
|
||||
hide();
|
||||
sendSelectedUtxos(treeTableView, hashIndexEntry);
|
||||
@@ -444,20 +529,32 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
if(hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) && !hashIndexEntry.getHashIndex().isSpent()) {
|
||||
if(hashIndexEntry.getHashIndex().getStatus() == null || hashIndexEntry.getHashIndex().getStatus() != Status.FROZEN) {
|
||||
MenuItem freezeUtxo = new MenuItem("Freeze UTXO");
|
||||
freezeUtxo.setGraphic(getFreezeGlyph());
|
||||
freezeUtxo.setOnAction(AE -> {
|
||||
hide();
|
||||
freezeUtxo(hashIndexEntry);
|
||||
freezeUtxo(treeTableView, hashIndexEntry);
|
||||
});
|
||||
getItems().add(freezeUtxo);
|
||||
} else {
|
||||
MenuItem unfreezeUtxo = new MenuItem("Unfreeze UTXO");
|
||||
unfreezeUtxo.setGraphic(getUnfreezeGlyph());
|
||||
unfreezeUtxo.setOnAction(AE -> {
|
||||
hide();
|
||||
unfreezeUtxo(hashIndexEntry);
|
||||
unfreezeUtxo(treeTableView, hashIndexEntry);
|
||||
});
|
||||
getItems().add(unfreezeUtxo);
|
||||
}
|
||||
}
|
||||
|
||||
String label = "Copy " + (hashIndexEntry.getType().equals(HashIndexEntry.Type.OUTPUT) ? "Transaction Output" : "Transaction Input");
|
||||
MenuItem copyHashIndex = new MenuItem(label);
|
||||
copyHashIndex.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(hashIndexEntry.getHashIndex().toString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyHashIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.google.gson.JsonParseException;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
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.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
@@ -113,8 +114,9 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
importButton.setDisable(true);
|
||||
setExpanded(true);
|
||||
} else {
|
||||
InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
|
||||
importFile(file.getName(), inputStream, password);
|
||||
try(InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||
importFile(file.getName(), inputStream, password);
|
||||
};
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error importing file", e);
|
||||
@@ -171,11 +173,15 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
if(wallets != null) {
|
||||
for(Wallet wallet : wallets) {
|
||||
if(scriptType.equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
|
||||
return wallet.getKeystores().get(0);
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
keystore.setLabel(importer.getName().replace(" Multisig", ""));
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(importer.getWalletModel());
|
||||
return keystore;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ImportException("Script type " + scriptType + " is not supported");
|
||||
throw new ImportException("Script type " + scriptType.getDescription() + " is not supported in this QR. Check you are displaying the correct QR code.");
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -9,10 +9,7 @@ import com.sparrowwallet.sparrow.event.StorageEvent;
|
||||
import com.sparrowwallet.sparrow.event.TimedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletExportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.CoboVaultMultisig;
|
||||
import com.sparrowwallet.sparrow.io.PassportMultisig;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.io.WalletExport;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Control;
|
||||
@@ -83,8 +80,8 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
|
||||
String extension = exporter.getExportFileExtension(wallet);
|
||||
fileChooser.setInitialFileName(wallet.getName() + "-" +
|
||||
exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "") +
|
||||
fileChooser.setInitialFileName(wallet.getName() +
|
||||
(exporter instanceof Sparrow ? "" : "-" + exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "")) +
|
||||
(extension == null || extension.isEmpty() ? "" : "." + extension));
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
@@ -100,10 +97,10 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
final File walletFile = AppServices.get().getOpenWallets().get(wallet).getWalletFile();
|
||||
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Done"));
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
|
||||
try {
|
||||
@@ -113,10 +110,10 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
}
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
setError("Export Error", decryptWalletService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(walletFile, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
} else {
|
||||
@@ -127,16 +124,17 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
private void exportWallet(File file, Wallet exportWallet) {
|
||||
try {
|
||||
if(file != null) {
|
||||
OutputStream outputStream = new FileOutputStream(file);
|
||||
exporter.exportWallet(exportWallet, outputStream);
|
||||
EventManager.get().post(new WalletExportEvent(exportWallet));
|
||||
try(OutputStream outputStream = new FileOutputStream(file)) {
|
||||
exporter.exportWallet(exportWallet, outputStream);
|
||||
EventManager.get().post(new WalletExportEvent(exportWallet));
|
||||
}
|
||||
} else {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
exporter.exportWallet(exportWallet, outputStream);
|
||||
QRDisplayDialog qrDisplayDialog;
|
||||
if(exporter instanceof CoboVaultMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
||||
} else if(exporter instanceof PassportMultisig) {
|
||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
@@ -27,6 +28,8 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class);
|
||||
@@ -42,35 +45,62 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
|
||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||
this.fileName = fileName;
|
||||
try {
|
||||
fileBytes = ByteStreams.toByteArray(inputStream);
|
||||
} catch(IOException e) {
|
||||
throw new ImportException("Could not read file", e);
|
||||
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||
if(wallets != null && !wallets.isEmpty()) {
|
||||
if(wallets.size() == 1 && scriptTypes.contains(wallets.get(0).getScriptType())) {
|
||||
Wallet wallet = wallets.get(0);
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
|
||||
wallet.setName(importer.getName());
|
||||
EventManager.get().post(new WalletImportEvent(wallets.get(0)));
|
||||
} else {
|
||||
scriptTypes.retainAll(wallets.stream().map(Wallet::getScriptType).collect(Collectors.toList()));
|
||||
if(scriptTypes.isEmpty()) {
|
||||
throw new ImportException("No singlesig script types present in QR code");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
fileBytes = ByteStreams.toByteArray(inputStream);
|
||||
} catch(IOException e) {
|
||||
throw new ImportException("Could not read file", e);
|
||||
}
|
||||
}
|
||||
|
||||
setContent(getScriptTypeEntry());
|
||||
setContent(getScriptTypeEntry(scriptTypes));
|
||||
setExpanded(true);
|
||||
importButton.setDisable(true);
|
||||
}
|
||||
|
||||
private void importWallet(ScriptType scriptType) throws ImportException {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
||||
Keystore keystore = importer.getKeystore(scriptType, bais, "");
|
||||
if(wallets != null && !wallets.isEmpty()) {
|
||||
Wallet wallet = wallets.stream().filter(wallet1 -> wallet1.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
|
||||
wallet.setName(importer.getName());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
||||
Keystore keystore = importer.getKeystore(scriptType, bais, "");
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(fileName);
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(Files.getNameWithoutExtension(fileName));
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
}
|
||||
}
|
||||
|
||||
private Node getScriptTypeEntry() {
|
||||
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
|
||||
Label label = new Label("Script Type:");
|
||||
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
|
||||
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
|
||||
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
|
||||
if(scriptTypes.contains(ScriptType.P2WPKH)) {
|
||||
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
|
||||
}
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
|
||||
@@ -6,6 +6,7 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTreeTableCell;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.DataFormat;
|
||||
import javafx.util.converter.DefaultStringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -28,10 +29,11 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
EntryCell.applyRowStyles(this, getTreeTableView().getTreeItem(getIndex()).getValue());
|
||||
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
||||
EntryCell.applyRowStyles(this, entry);
|
||||
|
||||
setText(label);
|
||||
setContextMenu(new LabelContextMenu(label));
|
||||
setContextMenu(new LabelContextMenu(entry, label));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +82,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
}
|
||||
|
||||
private static class LabelContextMenu extends ContextMenu {
|
||||
public LabelContextMenu(String label) {
|
||||
public LabelContextMenu(Entry entry, String label) {
|
||||
MenuItem copyLabel = new MenuItem("Copy Label");
|
||||
copyLabel.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -88,8 +90,20 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
content.putString(label);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().add(copyLabel);
|
||||
|
||||
Object content = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
|
||||
if(content instanceof String) {
|
||||
MenuItem pasteLabel = new MenuItem("Paste Label");
|
||||
pasteLabel.setOnAction(AE -> {
|
||||
hide();
|
||||
Object currentContent = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
|
||||
if(currentContent instanceof String) {
|
||||
entry.labelProperty().set((String)currentContent);
|
||||
}
|
||||
});
|
||||
getItems().add(pasteLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -24,6 +24,7 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
@@ -34,6 +35,7 @@ import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.security.SignatureException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -43,8 +45,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private final TextField address;
|
||||
private final TextArea message;
|
||||
private final TextArea signature;
|
||||
private final ToggleGroup formatGroup;
|
||||
private final ToggleButton formatTrezor;
|
||||
private final ToggleButton formatElectrum;
|
||||
private final Wallet wallet;
|
||||
private WalletNode walletNode;
|
||||
private boolean electrumSignatureFormat;
|
||||
private boolean closed;
|
||||
|
||||
/**
|
||||
* Verification only constructor
|
||||
@@ -69,6 +76,19 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
* @param walletNode Wallet node to derive address from
|
||||
*/
|
||||
public MessageSignDialog(Wallet wallet, WalletNode walletNode) {
|
||||
this(wallet, walletNode, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign and verify with preset address, and supplied title, message and dialog buttons
|
||||
*
|
||||
* @param wallet Wallet to sign with
|
||||
* @param walletNode Wallet node to derive address from
|
||||
* @param title Header text of dialog
|
||||
* @param msg Message to sign (all fields will be made uneditable)
|
||||
* @param buttons The dialog buttons to display. If one contains the text "sign" it will trigger the signing process
|
||||
*/
|
||||
public MessageSignDialog(Wallet wallet, WalletNode walletNode, String title, String msg, ButtonType... buttons) {
|
||||
if(wallet != null) {
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
|
||||
@@ -84,7 +104,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText(wallet == null ? "Verify Message" : "Sign/Verify Message");
|
||||
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
|
||||
|
||||
Image image = new Image("image/seed.png", 50, 50, false, false);
|
||||
if (!image.isError()) {
|
||||
@@ -129,51 +149,90 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
signature.setWrapText(true);
|
||||
signatureField.getInputs().add(signature);
|
||||
|
||||
fieldset.getChildren().addAll(addressField, messageField, signatureField);
|
||||
Field formatField = new Field();
|
||||
formatField.setText("Format:");
|
||||
formatGroup = new ToggleGroup();
|
||||
formatElectrum = new ToggleButton("Standard (Electrum)");
|
||||
formatTrezor = new ToggleButton("BIP137 (Trezor)");
|
||||
SegmentedButton formatButtons = new SegmentedButton(formatElectrum, formatTrezor);
|
||||
formatButtons.setToggleGroup(formatGroup);
|
||||
formatField.getInputs().add(formatButtons);
|
||||
|
||||
formatGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
electrumSignatureFormat = (newValue == formatElectrum);
|
||||
});
|
||||
|
||||
formatButtons.setDisable(wallet != null && walletNode != null && wallet.getScriptType() == ScriptType.P2PKH);
|
||||
|
||||
fieldset.getChildren().addAll(addressField, messageField, signatureField, formatField);
|
||||
form.getChildren().add(fieldset);
|
||||
dialogPane.setContent(form);
|
||||
|
||||
if(msg != null) {
|
||||
message.setText(msg);
|
||||
address.setEditable(false);
|
||||
message.setEditable(false);
|
||||
signature.setEditable(false);
|
||||
formatButtons.setDisable(true);
|
||||
}
|
||||
|
||||
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);
|
||||
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
|
||||
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
Button signButton = (Button)dialogPane.lookupButton(signButtonType);
|
||||
signButton.setDisable(wallet == null);
|
||||
signButton.setOnAction(event -> {
|
||||
signMessage();
|
||||
});
|
||||
if(buttons.length > 0) {
|
||||
dialogPane.getButtonTypes().addAll(buttons);
|
||||
|
||||
Button verifyButton = (Button)dialogPane.lookupButton(verifyButtonType);
|
||||
verifyButton.setDefaultButton(false);
|
||||
verifyButton.setOnAction(event -> {
|
||||
verifyMessage();
|
||||
});
|
||||
|
||||
boolean validAddress = isValidAddress();
|
||||
signButton.setDisable(!validAddress || (wallet == null));
|
||||
verifyButton.setDisable(!validAddress);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.registerValidator(address, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !isValidAddress()));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
});
|
||||
|
||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
boolean valid = isValidAddress();
|
||||
signButton.setDisable(!valid || (wallet == null));
|
||||
verifyButton.setDisable(!valid);
|
||||
|
||||
if(valid && wallet != null) {
|
||||
try {
|
||||
Address address = getAddress();
|
||||
setWalletNodeFromAddress(wallet, address);
|
||||
} catch(InvalidAddressException e) {
|
||||
//can't happen
|
||||
}
|
||||
ButtonType customSignButtonType = Arrays.stream(buttons).filter(buttonType -> buttonType.getText().toLowerCase().contains("sign")).findFirst().orElse(null);
|
||||
if(customSignButtonType != null) {
|
||||
Button customSignButton = (Button)dialogPane.lookupButton(customSignButtonType);
|
||||
customSignButton.setDefaultButton(true);
|
||||
customSignButton.setOnAction(event -> {
|
||||
customSignButton.setDisable(true);
|
||||
signMessage();
|
||||
setResult(ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
dialogPane.getButtonTypes().addAll(signButtonType, verifyButtonType, doneButtonType);
|
||||
|
||||
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
|
||||
signButton.setDisable(wallet == null);
|
||||
signButton.setOnAction(event -> {
|
||||
signMessage();
|
||||
});
|
||||
|
||||
Button verifyButton = (Button) dialogPane.lookupButton(verifyButtonType);
|
||||
verifyButton.setDefaultButton(false);
|
||||
verifyButton.setOnAction(event -> {
|
||||
verifyMessage();
|
||||
});
|
||||
|
||||
boolean validAddress = isValidAddress();
|
||||
signButton.setDisable(!validAddress || (wallet == null));
|
||||
verifyButton.setDisable(!validAddress);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(address, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !isValidAddress()));
|
||||
});
|
||||
|
||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
boolean valid = isValidAddress();
|
||||
signButton.setDisable(!valid || (wallet == null));
|
||||
verifyButton.setDisable(!valid);
|
||||
|
||||
if(valid && wallet != null) {
|
||||
try {
|
||||
Address address = getAddress();
|
||||
setWalletNodeFromAddress(wallet, address);
|
||||
} catch(InvalidAddressException e) {
|
||||
//can't happen
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
@@ -182,21 +241,49 @@ 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 : ButtonBar.ButtonData.OK_DONE);
|
||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if(address.getText().isEmpty()) {
|
||||
address.requestFocus();
|
||||
} else if(message.getText().isEmpty()) {
|
||||
message.requestFocus();
|
||||
}
|
||||
|
||||
formatGroup.selectToggle(formatElectrum);
|
||||
});
|
||||
}
|
||||
|
||||
private Address getAddress()throws InvalidAddressException {
|
||||
return Address.fromString(address.getText());
|
||||
}
|
||||
|
||||
public String getSignature() {
|
||||
return signature.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the Electrum signing format, which uses the non-segwit compressed signing parameters for both segwit types (p2sh-p2wpkh and p2wpkh)
|
||||
*
|
||||
* @param electrumSignatureFormat
|
||||
*/
|
||||
public void setElectrumSignatureFormat(boolean electrumSignatureFormat) {
|
||||
formatGroup.selectToggle(electrumSignatureFormat ? formatElectrum : formatTrezor);
|
||||
this.electrumSignatureFormat = electrumSignatureFormat;
|
||||
}
|
||||
|
||||
private boolean isValidAddress() {
|
||||
try {
|
||||
getAddress();
|
||||
return true;
|
||||
Address address = getAddress();
|
||||
return address.getScriptType() != ScriptType.P2TR;
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -228,7 +315,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
//Note we can expect a single keystore due to the check above
|
||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
String signatureText = privKey.signMessage(message.getText().trim(), decryptedWallet.getScriptType(), null);
|
||||
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||
String signatureText = privKey.signMessage(message.getText().trim(), scriptType);
|
||||
signature.clear();
|
||||
signature.appendText(signatureText);
|
||||
privKey.clear();
|
||||
@@ -316,16 +404,16 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Done"));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
signUnencryptedKeystore(decryptedWallet);
|
||||
decryptedWallet.clearPrivate();
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.END, "Failed"));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
|
||||
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletFile(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -36,7 +37,6 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
@@ -159,15 +159,21 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
|
||||
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
WordEntry wordEntry = new WordEntry(i, wordEntryList);
|
||||
wordsPane.getChildren().add(wordEntry);
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
|
||||
}
|
||||
wordsPane.getChildren().addAll(wordEntries);
|
||||
|
||||
vBox.getChildren().add(wordsPane);
|
||||
|
||||
if(!displayWordsOnly) {
|
||||
PassphraseEntry passphraseEntry = new PassphraseEntry();
|
||||
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
|
||||
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
|
||||
vBox.getChildren().add(passphraseEntry);
|
||||
|
||||
@@ -397,6 +403,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
private static class WordEntry extends HBox {
|
||||
private static List<String> wordList;
|
||||
private final TextField wordField;
|
||||
private WordEntry nextEntry;
|
||||
private TextField nextField;
|
||||
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
|
||||
super();
|
||||
@@ -406,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();
|
||||
@@ -425,13 +455,18 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
|
||||
autoCompletionBinding.setDelay(50);
|
||||
autoCompletionBinding.setOnAutoCompleted(event -> {
|
||||
if(nextField != null) {
|
||||
nextField.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(wordField, Validator.combine(
|
||||
Validator.createEmptyValidator("Word is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue))
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
|
||||
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
wordEntryList.set(wordNumber, newValue);
|
||||
@@ -444,6 +479,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
return wordField;
|
||||
}
|
||||
|
||||
public void setNextEntry(WordEntry nextEntry) {
|
||||
this.nextEntry = nextEntry;
|
||||
}
|
||||
|
||||
public void setNextField(TextField field) {
|
||||
this.nextField = field;
|
||||
}
|
||||
|
||||
public static boolean isValid(String word) {
|
||||
return wordList.contains(word);
|
||||
}
|
||||
@@ -461,10 +504,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
for(String word : wordList) {
|
||||
if(word.equals(request.getUserText())) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
if(word.startsWith(request.getUserText())) {
|
||||
suggestions.add(word);
|
||||
}
|
||||
@@ -476,13 +515,15 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private class PassphraseEntry extends HBox {
|
||||
private final CustomTextField passphraseField;
|
||||
|
||||
public PassphraseEntry() {
|
||||
super();
|
||||
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setSpacing(10);
|
||||
Label passphraseLabel = new Label("Passphrase:");
|
||||
CustomTextField passphraseField = (CustomTextField) TextFields.createClearableTextField();
|
||||
passphraseField = (CustomTextField) TextFields.createClearableTextField();
|
||||
passphraseProperty.bind(passphraseField.textProperty());
|
||||
passphraseField.setPromptText("Leave blank for none");
|
||||
|
||||
@@ -492,6 +533,10 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
|
||||
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
|
||||
}
|
||||
|
||||
public TextField getEditor() {
|
||||
return passphraseField;
|
||||
}
|
||||
}
|
||||
|
||||
private Node getDerivationEntry(List<ChildNumber> derivation) {
|
||||
@@ -501,11 +546,11 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(derivationField, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
|
||||
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
|
||||
importDerivationButton.setDisable(true);
|
||||
|
||||
@@ -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);
|
||||
@@ -113,7 +122,22 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
webcamService.resultProperty().addListener(new QRResultListener());
|
||||
webcamService.setOnFailed(failedEvent -> {
|
||||
Platform.runLater(() -> setResult(new Result(failedEvent.getSource().getException())));
|
||||
Throwable exception = failedEvent.getSource().getException();
|
||||
|
||||
Throwable nested = exception;
|
||||
while(nested.getCause() != null) {
|
||||
nested = nested.getCause();
|
||||
}
|
||||
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;
|
||||
Platform.runLater(() -> setResult(new Result(result)));
|
||||
});
|
||||
webcamService.start();
|
||||
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
|
||||
@@ -122,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);
|
||||
@@ -134,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);
|
||||
@@ -168,7 +199,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
} else {
|
||||
decoder.receivePart(qrtext);
|
||||
Platform.runLater(() -> percentComplete.setValue(decoder.getEstimatedPercentComplete()));
|
||||
Platform.runLater(() -> percentComplete.setValue(decoder.getProcessedPartsCount() > 0 ? decoder.getEstimatedPercentComplete() : 0));
|
||||
|
||||
if(decoder.getResult() != null) {
|
||||
URDecoder.Result urResult = decoder.getResult();
|
||||
@@ -197,7 +228,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
if(parts.stream().filter(Objects::nonNull).count() == n) {
|
||||
String complete = String.join("", parts);
|
||||
try {
|
||||
PSBT psbt = PSBT.fromString(complete);
|
||||
PSBT psbt = PSBT.fromString(complete, false);
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(PSBTParseException e) {
|
||||
@@ -249,7 +280,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
|
||||
try {
|
||||
psbt = PSBT.fromString(qrtext);
|
||||
psbt = PSBT.fromString(qrtext, false);
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(PSBTParseException e) {
|
||||
@@ -261,7 +292,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
|
||||
try {
|
||||
psbt = new PSBT(qrResult.getRawBytes());
|
||||
psbt = new PSBT(qrResult.getRawBytes(), false);
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
@@ -286,7 +317,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
//Try Base43 used by Electrum
|
||||
try {
|
||||
psbt = new PSBT(Base43.decode(qrtext));
|
||||
psbt = new PSBT(Base43.decode(qrtext), false);
|
||||
result = new Result(psbt);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
@@ -312,7 +343,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
if(urRegistryType.equals(RegistryType.BYTES)) {
|
||||
byte[] urBytes = (byte[])ur.decodeFromRegistry();
|
||||
try {
|
||||
PSBT psbt = new PSBT(urBytes);
|
||||
PSBT psbt = new PSBT(urBytes, false);
|
||||
return new Result(psbt);
|
||||
} catch(PSBTParseException e) {
|
||||
if(PSBT.isPSBT(urBytes)) {
|
||||
@@ -342,7 +373,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_PSBT)) {
|
||||
CryptoPSBT cryptoPSBT = (CryptoPSBT)ur.decodeFromRegistry();
|
||||
try {
|
||||
PSBT psbt = new PSBT(cryptoPSBT.getPsbt());
|
||||
PSBT psbt = new PSBT(cryptoPSBT.getPsbt(), false);
|
||||
return new Result(psbt);
|
||||
} catch(Exception e) {
|
||||
log.error("Error parsing PSBT from UR type " + urRegistryType, e);
|
||||
@@ -457,7 +488,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey();
|
||||
wallet.setScriptType(outputDescriptor.getScriptType());
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, outputDescriptor.getKeyDerivation(extendedKey).getDerivationPath()));
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(outputDescriptor.getKeyDerivation(extendedKey).getDerivation())));
|
||||
keystore.setExtendedPublicKey(extendedKey);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallets.add(wallet);
|
||||
@@ -508,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();
|
||||
@@ -546,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;
|
||||
}
|
||||
@@ -558,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,6 +769,42 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
}
|
||||
|
||||
public static class WebcamDependencyException extends ScanException {
|
||||
public WebcamDependencyException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public WebcamDependencyException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public WebcamDependencyException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public WebcamDependencyException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.csvreader.CsvReader;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.control.spreadsheet.*;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
private final BitcoinUnit bitcoinUnit;
|
||||
private final SpreadsheetView spreadsheetView;
|
||||
public static final AddressCellType ADDRESS = new AddressCellType();
|
||||
|
||||
public SendToManyDialog(BitcoinUnit bitcoinUnit) {
|
||||
this.bitcoinUnit = bitcoinUnit;
|
||||
|
||||
final DialogPane dialogPane = new SendToManyDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
setTitle("Send to Many");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.setHeaderText("Send to many recipients by specifying addresses and amounts.\nOnly the first row's label is necessary.");
|
||||
Image image = new Image("/image/sparrow-small.png");
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
|
||||
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
|
||||
Grid grid = getGrid(initialPayments);
|
||||
|
||||
spreadsheetView = new SpreadsheetView(grid);
|
||||
spreadsheetView.getColumns().get(0).setPrefWidth(400);
|
||||
spreadsheetView.getColumns().get(1).setPrefWidth(150);
|
||||
spreadsheetView.getColumns().get(2).setPrefWidth(247);
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(spreadsheetView);
|
||||
dialogPane.setContent(stackPane);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load CSV", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
||||
|
||||
setResultConverter((dialogButton) -> {
|
||||
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||
return data == ButtonBar.ButtonData.OK_DONE ? getPayments() : null;
|
||||
});
|
||||
|
||||
dialogPane.setPrefWidth(850);
|
||||
dialogPane.setPrefHeight(500);
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
private Grid getGrid(List<Payment> payments) {
|
||||
int rowCount = payments.size();
|
||||
int columnCount = 3;
|
||||
GridBase grid = new GridBase(rowCount, columnCount);
|
||||
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
|
||||
for(int row = 0; row < grid.getRowCount(); ++row) {
|
||||
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
|
||||
|
||||
SpreadsheetCell addressCell = ADDRESS.createCell(row, 0, 1, 1, payments.get(row).getAddress());
|
||||
addressCell.getStyleClass().add("fixed-width");
|
||||
list.add(addressCell);
|
||||
|
||||
double amount = (double)payments.get(row).getAmount();
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
SpreadsheetCell amountCell = SpreadsheetCellType.DOUBLE.createCell(row, 1, 1, 1, amount < 0 ? null : amount);
|
||||
amountCell.setFormat(bitcoinUnit == BitcoinUnit.BTC ? "0.00000000" : "###,###");
|
||||
amountCell.getStyleClass().add("number-value");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
amountCell.getStyleClass().add("number-field");
|
||||
}
|
||||
list.add(amountCell);
|
||||
|
||||
list.add(SpreadsheetCellType.STRING.createCell(row, 2, 1, 1, payments.get(row).getLabel()));
|
||||
rows.add(list);
|
||||
}
|
||||
grid.setRows(rows);
|
||||
grid.getColumnHeaders().setAll("Address", "Amount (" + bitcoinUnit.getLabel() + ")", "Label");
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
private List<Payment> getPayments() {
|
||||
List<Payment> payments = new ArrayList<>();
|
||||
Grid grid = spreadsheetView.getGrid();
|
||||
String firstLabel = null;
|
||||
for(int row = 0; row < grid.getRowCount(); row++) {
|
||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||
Address address = (Address)rowCells.get(0).getItem();
|
||||
Double value = (Double)rowCells.get(1).getItem();
|
||||
String label = (String)rowCells.get(2).getItem();
|
||||
if(firstLabel == null) {
|
||||
firstLabel = label;
|
||||
}
|
||||
if(label == null || label.isEmpty()) {
|
||||
label = firstLabel;
|
||||
}
|
||||
|
||||
if(address != null && value != null) {
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
|
||||
payments.add(new Payment(address, label, value.longValue(), false));
|
||||
}
|
||||
}
|
||||
|
||||
return payments;
|
||||
}
|
||||
|
||||
private class SendToManyDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
Node button;
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
Button loadButton = new Button(buttonType.getText());
|
||||
loadButton.setGraphicTextGap(5);
|
||||
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(loadButton, buttonData);
|
||||
loadButton.setOnAction(event -> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open CSV");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("CSV", "*.csv")
|
||||
);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(this.getScene().getWindow(), 800, 450);
|
||||
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
|
||||
if(file != null) {
|
||||
try {
|
||||
List<Payment> csvPayments = new ArrayList<>();
|
||||
try(Reader reader = new FileReader(file, StandardCharsets.UTF_8)) {
|
||||
CsvReader csvReader = new CsvReader(reader);
|
||||
while(csvReader.readRecord()) {
|
||||
if(csvReader.getColumnCount() < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
long amount;
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
double doubleAmount = Double.parseDouble(csvReader.get(1).replace(",", ""));
|
||||
amount = (long)(doubleAmount * Transaction.SATOSHIS_PER_BITCOIN);
|
||||
} else {
|
||||
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
|
||||
}
|
||||
Address address = Address.fromString(csvReader.get(0));
|
||||
String label = csvReader.get(2);
|
||||
csvPayments.add(new Payment(address, label, amount, false));
|
||||
} catch(NumberFormatException e) {
|
||||
//ignore and continue - probably a header line
|
||||
} catch(InvalidAddressException e) {
|
||||
AppServices.showErrorDialog("Invalid Address", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if(csvPayments.isEmpty()) {
|
||||
AppServices.showErrorDialog("No recipients found", "No valid recipients were found. Use a CSV file with three columns, and ensure amounts are in " + bitcoinUnit.getLabel() + ".");
|
||||
return;
|
||||
}
|
||||
|
||||
spreadsheetView.setGrid(getGrid(csvPayments));
|
||||
}
|
||||
} catch(IOException e) {
|
||||
AppServices.showErrorDialog("Cannot load CSV", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
button = loadButton;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AddressCellType extends SpreadsheetCellType<Address> {
|
||||
public AddressCellType() {
|
||||
this(new StringConverterWithFormat<>(new AddressStringConverter()) {
|
||||
@Override
|
||||
public String toString(Address item) {
|
||||
return toStringFormat(item, ""); //$NON-NLS-1$
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address fromString(String str) {
|
||||
if(str == null || str.isEmpty()) { //$NON-NLS-1$
|
||||
return null;
|
||||
} else {
|
||||
return myConverter.fromString(str);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toStringFormat(Address item, String format) {
|
||||
try {
|
||||
if(item == null) {
|
||||
return ""; //$NON-NLS-1$
|
||||
} else {
|
||||
return item.toString();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
return myConverter.toString(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public AddressCellType(StringConverter<Address> converter) {
|
||||
super(converter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "address";
|
||||
}
|
||||
|
||||
public SpreadsheetCell createCell(final int row, final int column, final int rowSpan, final int columnSpan,
|
||||
final Address value) {
|
||||
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
||||
cell.setItem(value);
|
||||
return cell;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpreadsheetCellEditor createEditor(SpreadsheetView view) {
|
||||
return new SpreadsheetCellEditor.StringEditor(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(Object value, Object... options) {
|
||||
if(value instanceof Address)
|
||||
return true;
|
||||
else {
|
||||
try {
|
||||
converter.fromString(value == null ? null : value.toString());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address convertValue(Object value) {
|
||||
if(value instanceof Address)
|
||||
return (Address)value;
|
||||
else {
|
||||
try {
|
||||
return converter.fromString(value == null ? null : value.toString());
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Address item) {
|
||||
return converter.toString(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Address item, String format) {
|
||||
return ((StringConverterWithFormat<Address>)converter).toStringFormat(item, format);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -12,10 +12,14 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class TextAreaDialog extends Dialog<String> {
|
||||
private static final Logger log = LoggerFactory.getLogger(TextAreaDialog.class);
|
||||
|
||||
private final TextArea textArea;
|
||||
private final String defaultValue;
|
||||
|
||||
@@ -99,6 +103,7 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
} else if(result.outputDescriptor != null) {
|
||||
textArea.setText(result.outputDescriptor.toString(true));
|
||||
} else if(result.exception != null) {
|
||||
log.error("Error scanning QR", result.exception);
|
||||
AppServices.showErrorDialog("Error scanning QR", result.exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.util.Arrays;
|
||||
import java.util.OptionalDouble;
|
||||
|
||||
public class TitledDescriptionPane extends TitledPane {
|
||||
private Label mainLabel;
|
||||
private Label descriptionLabel;
|
||||
protected Hyperlink showHideLink;
|
||||
protected HBox buttonBox;
|
||||
@@ -50,7 +51,7 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
VBox labelsBox = new VBox();
|
||||
labelsBox.setSpacing(5);
|
||||
labelsBox.setAlignment(Pos.CENTER_LEFT);
|
||||
Label mainLabel = new Label();
|
||||
mainLabel = new Label();
|
||||
mainLabel.setText(title);
|
||||
mainLabel.getStyleClass().add("main-label");
|
||||
labelsBox.getChildren().add(mainLabel);
|
||||
@@ -99,6 +100,10 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return mainLabel.getText();
|
||||
}
|
||||
|
||||
protected void setDescription(String text) {
|
||||
descriptionLabel.getStyleClass().remove("description-error");
|
||||
descriptionLabel.getStyleClass().add("description-label");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
@@ -10,6 +11,7 @@ import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -200,6 +202,7 @@ public class TransactionDiagram extends GridPane {
|
||||
private Pane getInputsLabels(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) {
|
||||
VBox inputsBox = new VBox();
|
||||
inputsBox.setMaxWidth(150);
|
||||
inputsBox.setPrefWidth(150);
|
||||
inputsBox.setPadding(new Insets(0, 10, 0, 10));
|
||||
inputsBox.minHeightProperty().bind(minHeightProperty());
|
||||
inputsBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -388,14 +391,33 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
if(walletTx.getChangeNode() != null) {
|
||||
WalletNode defaultChangeNode = walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE);
|
||||
boolean overGapLimit = (walletTx.getChangeNode().getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
String changeDesc = walletTx.getChangeAddress().toString().substring(0, 8) + "...";
|
||||
Label changeLabel = new Label(changeDesc, getChangeGlyph());
|
||||
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
|
||||
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + walletTx.getChangeNode().getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress().toString());
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(walletTx.getChangeAmount()) + " sats to " + walletTx.getChangeNode().getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress().toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
|
||||
changeTooltip.getStyleClass().add("change-label");
|
||||
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
changeLabel.setTooltip(changeTooltip);
|
||||
outputsBox.getChildren().add(changeLabel);
|
||||
|
||||
Button nextChangeAddressButton = new Button("");
|
||||
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
|
||||
nextChangeAddressButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
|
||||
});
|
||||
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
|
||||
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
|
||||
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
|
||||
replaceChangeLabel.getStyleClass().add("replace-change-label");
|
||||
replaceChangeLabel.setVisible(false);
|
||||
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
|
||||
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
|
||||
|
||||
actionBox.getChildren().addAll(changeLabel, replaceChangeLabel);
|
||||
outputsBox.getChildren().add(actionBox);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
}
|
||||
|
||||
@@ -484,6 +506,20 @@ public class TransactionDiagram extends GridPane {
|
||||
return changeGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getChangeWarningGlyph() {
|
||||
Glyph changeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
changeWarningGlyph.getStyleClass().add("change-warning-icon");
|
||||
changeWarningGlyph.setFontSize(12);
|
||||
return changeWarningGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getChangeReplaceGlyph() {
|
||||
Glyph changeReplaceGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||
changeReplaceGlyph.getStyleClass().add("change-replace-icon");
|
||||
changeReplaceGlyph.setFontSize(12);
|
||||
return changeReplaceGlyph;
|
||||
}
|
||||
|
||||
private Glyph getFeeGlyph() {
|
||||
Glyph feeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING);
|
||||
feeGlyph.getStyleClass().add("fee-icon");
|
||||
|
||||
@@ -4,7 +4,11 @@ import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.stage.Popup;
|
||||
import org.fxmisc.richtext.CodeArea;
|
||||
import org.fxmisc.richtext.event.MouseOverTextEvent;
|
||||
@@ -33,7 +37,8 @@ public class TransactionHexArea extends CodeArea {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
transaction.bitcoinSerializeToStream(baos);
|
||||
|
||||
String hex = Utils.bytesToHex(baos.toByteArray());
|
||||
String fullHex = Utils.bytesToHex(baos.toByteArray());
|
||||
String hex = fullHex;
|
||||
if(hex.length() > TRUNCATE_AT) {
|
||||
hex = hex.substring(0, TRUNCATE_AT);
|
||||
hex += "[truncated]";
|
||||
@@ -42,6 +47,7 @@ public class TransactionHexArea extends CodeArea {
|
||||
clear();
|
||||
appendText(hex);
|
||||
previousSegmentList = new ArrayList<>();
|
||||
setContextMenu(new TransactionHexContextMenu(fullHex));
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Can't happen");
|
||||
}
|
||||
@@ -266,4 +272,18 @@ public class TransactionHexArea extends CodeArea {
|
||||
return Objects.hash(start, length, style);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionHexContextMenu extends ContextMenu {
|
||||
public TransactionHexContextMenu(String hex) {
|
||||
MenuItem copy = new MenuItem("Copy All");
|
||||
copy.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(hex);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().add(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@ public class TransactionIdDialog extends Dialog<Sha256Hash> {
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(txid, Validator.combine(
|
||||
Validator.createEmptyValidator("Transaction id is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Transaction ID length incorrect", newValue.length() != 64),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Transaction ID must be hexadecimal", !Utils.isHex(newValue))
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
});
|
||||
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Open Transaction", ButtonBar.ButtonData.OK_DONE);
|
||||
|
||||
@@ -44,16 +44,18 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
|
||||
totalUtxos = utxoDataList.size();
|
||||
if(utxoDataList.size() > MAX_BARS) {
|
||||
Long otherTotal = utxoDataList.subList(MAX_BARS - 1, utxoDataList.size()).stream().mapToLong(data -> data.getYValue().longValue()).sum();
|
||||
List<Data<String, Number>> otherDataList = utxoDataList.subList(MAX_BARS - 1, utxoDataList.size());
|
||||
List<Entry> otherEntries = otherDataList.stream().map(data -> (Entry)data.getExtraValue()).collect(Collectors.toList());
|
||||
Long otherTotal = otherDataList.stream().mapToLong(data -> data.getYValue().longValue()).sum();
|
||||
utxoDataList = utxoDataList.subList(0, MAX_BARS - 1);
|
||||
utxoDataList.add(new XYChart.Data<>(OTHER_CATEGORY, otherTotal));
|
||||
utxoDataList.add(new XYChart.Data<>(OTHER_CATEGORY, otherTotal, otherEntries));
|
||||
}
|
||||
|
||||
for(int i = 0; i < utxoDataList.size(); i++) {
|
||||
XYChart.Data<String, Number> newData = utxoDataList.get(i);
|
||||
if(i < utxoSeries.getData().size()) {
|
||||
XYChart.Data<String, Number> existingData = utxoSeries.getData().get(i);
|
||||
if(!newData.getXValue().equals(existingData.getXValue()) || !newData.getYValue().equals(existingData.getYValue()) || (newData.getExtraValue() != null && !newData.getExtraValue().equals(existingData.getExtraValue()))) {
|
||||
if(!newData.getXValue().equals(existingData.getXValue()) || !newData.getYValue().equals(existingData.getYValue()) || (newData.getExtraValue() instanceof Entry && !newData.getExtraValue().equals(existingData.getExtraValue()))) {
|
||||
utxoSeries.getData().set(i, newData);
|
||||
}
|
||||
} else {
|
||||
@@ -87,7 +89,8 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
for(int i = 0; i < utxoSeries.getData().size(); i++) {
|
||||
XYChart.Data<String, Number> data = utxoSeries.getData().get(i);
|
||||
|
||||
if((data.getExtraValue() != null && entries.contains((Entry)data.getExtraValue())) || (data.getExtraValue() == null && entries.size() == totalUtxos)) {
|
||||
if((data.getExtraValue() instanceof Entry && entries.contains((Entry)data.getExtraValue())) ||
|
||||
(data.getExtraValue() instanceof List && entries.containsAll((List)data.getExtraValue()))) {
|
||||
Node bar = lookup(".data" + i);
|
||||
if(bar != null) {
|
||||
bar.getStyleClass().add("selected");
|
||||
|
||||
@@ -52,10 +52,10 @@ public class WalletBirthDateDialog extends Dialog<Date> {
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater( () -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(birthDatePicker, Validator.combine(
|
||||
(Control c, LocalDate newValue) -> ValidationResult.fromErrorIf( c, "Birth date not specified", newValue == null)
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
});
|
||||
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rescan Wallet", ButtonBar.ButtonData.OK_DONE);
|
||||
|
||||
@@ -11,6 +11,7 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class WalletExportDialog extends Dialog<Wallet> {
|
||||
@@ -43,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 PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), 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());
|
||||
}
|
||||
@@ -53,6 +54,8 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
FileWalletExportPane exportPane = new FileWalletExportPane(wallet, exporter);
|
||||
exportAccordion.getPanes().add(exportPane);
|
||||
}
|
||||
|
||||
exportAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||
scrollPane.setContent(exportAccordion);
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
@@ -16,6 +16,7 @@ import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class WalletImportDialog extends Dialog<Wallet> {
|
||||
@@ -47,17 +48,19 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
importAccordion = new Accordion();
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new PassportSinglesig());
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new SpecterDIY());
|
||||
for(KeystoreFileImport importer : keystoreImporters) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
}
|
||||
|
||||
List<WalletImport> walletImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new SpecterDesktop(), new BlueWalletMultisig());
|
||||
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);
|
||||
}
|
||||
|
||||
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||
scrollPane.setContent(importAccordion);
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
@@ -92,7 +95,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
List<Device> devices = enumerateService.getValue();
|
||||
importAccordion.getPanes().removeIf(titledPane -> titledPane instanceof DevicePane);
|
||||
for(Device device : devices) {
|
||||
DevicePane devicePane = new DevicePane(new Wallet(), device);
|
||||
DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1);
|
||||
importAccordion.getPanes().add(0, devicePane);
|
||||
}
|
||||
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
|
||||
|
||||
@@ -93,6 +93,7 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater( () -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(name, Validator.combine(
|
||||
Validator.createEmptyValidator("Wallet name is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Wallet name is not unique", Storage.walletExists(newValue))
|
||||
@@ -100,7 +101,6 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
|
||||
validationSupport.registerValidator(existingPicker, Validator.combine(
|
||||
(Control c, LocalDate newValue) -> ValidationResult.fromErrorIf( c, "Birth date not specified", existingCheck.isSelected() && newValue == null)
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
});
|
||||
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Create Wallet", ButtonBar.ButtonData.OK_DONE);
|
||||
|
||||
@@ -77,8 +77,8 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater( () -> {
|
||||
validationSupport.registerValidator(passwordConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Password confirmation does not match", !passwordConfirm.getText().equals(password.getText())));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(passwordConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Password confirmation does not match", !passwordConfirm.getText().equals(password.getText())));
|
||||
});
|
||||
|
||||
okButtonType = new javafx.scene.control.ButtonType(requirement.okButtonText, ButtonBar.ButtonData.OK_DONE);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -135,11 +135,11 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(xprvField, Validator.combine(
|
||||
Validator.createEmptyValidator("xprv is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid private key", !ExtendedKey.isValid(newValue) || ExtendedKey.fromDescriptor(newValue).getKey().isPubKeyOnly())
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
|
||||
Button importXprvButton = new Button("Import");
|
||||
importXprvButton.setMinWidth(80);
|
||||
@@ -176,11 +176,11 @@ public class XprvKeystoreImportPane extends TitledDescriptionPane {
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(derivationField, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
|
||||
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
|
||||
importDerivationButton.setDisable(true);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class KeystoreEncryptionChangedEvent extends WalletSettingsChangedEvent {
|
||||
private List<Keystore> changedKeystores;
|
||||
|
||||
public KeystoreEncryptionChangedEvent(Wallet wallet, Wallet pastWallet, String walletId, List<Keystore> changedKeystores) {
|
||||
super(wallet, pastWallet, walletId);
|
||||
this.changedKeystores = changedKeystores;
|
||||
}
|
||||
|
||||
public List<Keystore> getChangedKeystores() {
|
||||
return changedKeystores;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This event is trigger when one or more keystores on a wallet are updated, and the wallet is saved
|
||||
*/
|
||||
public class KeystoreLabelsChangedEvent extends WalletSettingsChangedEvent {
|
||||
private final List<Keystore> changedKeystores;
|
||||
|
||||
public KeystoreLabelsChangedEvent(Wallet wallet, Wallet pastWallet, String walletId, List<Keystore> changedKeystores) {
|
||||
super(wallet, pastWallet, walletId);
|
||||
this.changedKeystores = changedKeystores;
|
||||
}
|
||||
|
||||
public List<Keystore> getChangedKeystores() {
|
||||
return changedKeystores;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class LoadingLogChangedEvent {
|
||||
private final boolean visible;
|
||||
|
||||
public LoadingLogChangedEvent(boolean visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return visible;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||
|
||||
public class ReceiveActionEvent {
|
||||
private final NodeEntry receiveEntry;
|
||||
private final Wallet wallet;
|
||||
|
||||
public ReceiveActionEvent(NodeEntry receiveEntry) {
|
||||
this.receiveEntry = receiveEntry;
|
||||
this.wallet = receiveEntry.getWallet();
|
||||
}
|
||||
|
||||
public NodeEntry getReceiveEntry() {
|
||||
return receiveEntry;
|
||||
public ReceiveActionEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.net.Aopp;
|
||||
|
||||
public class ReceiveProofEvent {
|
||||
private final Wallet wallet;
|
||||
private final Aopp aopp;
|
||||
|
||||
public ReceiveProofEvent(Wallet wallet, Aopp aopp) {
|
||||
this.wallet = wallet;
|
||||
this.aopp = aopp;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public Aopp getAopp() {
|
||||
return aopp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
|
||||
public class ReplaceChangeAddressEvent {
|
||||
private final WalletTransaction walletTransaction;
|
||||
|
||||
public ReplaceChangeAddressEvent(WalletTransaction walletTx) {
|
||||
this.walletTransaction = walletTx;
|
||||
}
|
||||
|
||||
public WalletTransaction getWalletTransaction() {
|
||||
return walletTransaction;
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,30 @@ package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Event class used to request the transaction open file dialog
|
||||
*/
|
||||
public class RequestTransactionOpenEvent {
|
||||
private final Window window;
|
||||
private final File file;
|
||||
|
||||
public RequestTransactionOpenEvent(Window window) {
|
||||
this.window = window;
|
||||
this.file = null;
|
||||
}
|
||||
|
||||
public RequestTransactionOpenEvent(Window window, File file) {
|
||||
this.window = window;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public Window getWindow() {
|
||||
return window;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,30 @@ package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* Event class used to request the wallet open dialog
|
||||
*/
|
||||
public class RequestWalletOpenEvent {
|
||||
private final Window window;
|
||||
private final File file;
|
||||
|
||||
public RequestWalletOpenEvent(Window window) {
|
||||
this.window = window;
|
||||
this.file = null;
|
||||
}
|
||||
|
||||
public RequestWalletOpenEvent(Window window, File file) {
|
||||
this.window = window;
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public Window getWindow() {
|
||||
return window;
|
||||
}
|
||||
|
||||
public File getFile() {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SendPaymentsEvent {
|
||||
private final Wallet wallet;
|
||||
private final List<Payment> payments;
|
||||
|
||||
public SendPaymentsEvent(Wallet wallet, List<Payment> payments) {
|
||||
this.wallet = wallet;
|
||||
this.payments = payments;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public List<Payment> getPayments() {
|
||||
return payments;
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ import java.util.Map;
|
||||
|
||||
public class StorageEvent extends TimedEvent {
|
||||
private static boolean firstRunDone = false;
|
||||
private static final Map<File, Long> eventTime = new HashMap<>();
|
||||
private static final Map<String, Long> eventTime = new HashMap<>();
|
||||
|
||||
public StorageEvent(File file, Action action, String status) {
|
||||
public StorageEvent(String walletId, Action action, String status) {
|
||||
super(action, status);
|
||||
|
||||
Integer keyDerivationPeriod = Config.get().getKeyDerivationPeriod();
|
||||
@@ -19,10 +19,10 @@ public class StorageEvent extends TimedEvent {
|
||||
}
|
||||
|
||||
if(action == Action.START) {
|
||||
eventTime.put(file, System.currentTimeMillis());
|
||||
eventTime.put(walletId, System.currentTimeMillis());
|
||||
timeMills = keyDerivationPeriod;
|
||||
} else if(action == Action.END) {
|
||||
long start = eventTime.get(file);
|
||||
long start = eventTime.get(walletId);
|
||||
if(firstRunDone) {
|
||||
keyDerivationPeriod = (int)(System.currentTimeMillis() - start);
|
||||
Config.get().setKeyDerivationPeriod(keyDerivationPeriod);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class UtxosChartChangedEvent {
|
||||
private final boolean visible;
|
||||
|
||||
public UtxosChartChangedEvent(boolean visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return visible;
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,8 @@ import java.io.File;
|
||||
* This is because any failure in saving the wallet must be immediately reported to the user.
|
||||
* Note that all wallet detail controllers that share a WalletForm, and that class posts WalletNodesChangedEvent once it has cleared it's entry caches.
|
||||
*/
|
||||
public class WalletAddressesChangedEvent extends WalletSettingsChangedEvent {
|
||||
public WalletAddressesChangedEvent(Wallet wallet, Wallet pastWallet, File walletFile) {
|
||||
super(wallet, pastWallet, walletFile);
|
||||
public class WalletAddressesChangedEvent extends WalletHistoryClearedEvent {
|
||||
public WalletAddressesChangedEvent(Wallet wallet, Wallet pastWallet, String walletId) {
|
||||
super(wallet, pastWallet, walletId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
|
||||
/**
|
||||
* This event is fired when a wallet entry (transaction, txi or txo) label is changed.
|
||||
* Extends WalletDataChangedEvent so triggers a background save.
|
||||
*/
|
||||
public class WalletEntryLabelChangedEvent extends WalletDataChangedEvent {
|
||||
private final Entry entry;
|
||||
|
||||
public WalletEntryLabelChangedEvent(Wallet wallet, Entry entry) {
|
||||
super(wallet);
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
public Entry getEntry() {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This event is fired when a wallet entry (transaction, txi or txo) label is changed.
|
||||
*/
|
||||
public class WalletEntryLabelsChangedEvent extends WalletChangedEvent {
|
||||
private final List<Entry> entries;
|
||||
|
||||
public WalletEntryLabelsChangedEvent(Wallet wallet, Entry entry) {
|
||||
super(wallet);
|
||||
this.entries = List.of(entry);
|
||||
}
|
||||
|
||||
public WalletEntryLabelsChangedEvent(Wallet wallet, List<Entry> entries) {
|
||||
super(wallet);
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
public List<Entry> getEntries() {
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,8 @@ public class WalletHistoryChangedEvent extends WalletChangedEvent {
|
||||
this.historyChangedNodes = historyChangedNodes;
|
||||
}
|
||||
|
||||
public File getWalletFile() {
|
||||
return storage.getWalletFile();
|
||||
public String getWalletId() {
|
||||
return storage.getWalletId(getWallet());
|
||||
}
|
||||
|
||||
public List<WalletNode> getHistoryChangedNodes() {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletHistoryClearedEvent extends WalletSettingsChangedEvent {
|
||||
public WalletHistoryClearedEvent(Wallet wallet, Wallet pastWallet, String walletId) {
|
||||
super(wallet, pastWallet, walletId);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ public class WalletHistoryFailedEvent extends WalletHistoryStatusEvent {
|
||||
private final Throwable exception;
|
||||
|
||||
public WalletHistoryFailedEvent(Wallet wallet, Throwable exception) {
|
||||
super(wallet, exception.getCause() == null ? exception.getMessage() : exception.getCause().getMessage());
|
||||
super(wallet, exception.getMessage());
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletPasswordChangedEvent extends WalletSettingsChangedEvent {
|
||||
public WalletPasswordChangedEvent(Wallet wallet, Wallet pastWallet, String walletId) {
|
||||
super(wallet, pastWallet, walletId);
|
||||
}
|
||||
}
|
||||
@@ -5,25 +5,24 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* This event is posted when a wallet's settings are changed in a way that does not update the wallet addresses or history.
|
||||
* For example, changing the keystore source or label will trigger this event, which updates the wallet but avoids a full wallet refresh.
|
||||
* It is impossible for the validity of a wallet to change on this event - listen for WalletAddressesChangedEvent for this
|
||||
* This is the base class for events posted when a wallet's settings are changed
|
||||
* Do not listen for this event directly - listen for a subclass, for example KeystoreLabelsChangedEvent or WalletAddressesChangedEvent
|
||||
*/
|
||||
public class WalletSettingsChangedEvent extends WalletChangedEvent {
|
||||
private final Wallet pastWallet;
|
||||
private final File walletFile;
|
||||
private final String walletId;
|
||||
|
||||
public WalletSettingsChangedEvent(Wallet wallet, Wallet pastWallet, File walletFile) {
|
||||
public WalletSettingsChangedEvent(Wallet wallet, Wallet pastWallet, String walletId) {
|
||||
super(wallet);
|
||||
this.pastWallet = pastWallet;
|
||||
this.walletFile = walletFile;
|
||||
this.walletId = walletId;
|
||||
}
|
||||
|
||||
public Wallet getPastWallet() {
|
||||
return pastWallet;
|
||||
}
|
||||
|
||||
public File getWalletFile() {
|
||||
return walletFile;
|
||||
public String getWalletId() {
|
||||
return walletId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 WalletDataChangedEvent {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,13 @@ public class FontAwesome5 extends GlyphFont {
|
||||
ANGLE_DOUBLE_RIGHT('\uf101'),
|
||||
ARROW_DOWN('\uf063'),
|
||||
ARROW_UP('\uf062'),
|
||||
BAN('\uf05e'),
|
||||
BTC('\uf15a'),
|
||||
CAMERA('\uf030'),
|
||||
CHECK_CIRCLE('\uf058'),
|
||||
CIRCLE('\uf111'),
|
||||
COINS('\uf51e'),
|
||||
COPY('\uf0c5'),
|
||||
EXCHANGE_ALT('\uf362'),
|
||||
EXCLAMATION_CIRCLE('\uf06a'),
|
||||
EXCLAMATION_TRIANGLE('\uf071'),
|
||||
@@ -50,6 +52,8 @@ public class FontAwesome5 extends GlyphFont {
|
||||
SEARCH('\uf002'),
|
||||
SIGN_OUT_ALT('\uf2f5'),
|
||||
SQUARE('\uf0c8'),
|
||||
SNOWFLAKE('\uf2dc'),
|
||||
SUN('\uf185'),
|
||||
TIMES_CIRCLE('\uf057'),
|
||||
TOGGLE_OFF('\uf204'),
|
||||
TOGGLE_ON('\uf205'),
|
||||
|
||||
@@ -0,0 +1,535 @@
|
||||
/**
|
||||
* Copyright 2019 Pratanu Mandal
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.sparrowwallet.sparrow.instance;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.InetAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
|
||||
/**
|
||||
* The <code>Instance</code> class is the primary logical entry point to the library.<br>
|
||||
* It allows to create an application lock or free it and send and receive messages between first and subsequent instances.<br><br>
|
||||
*
|
||||
* <pre>
|
||||
* // unique application ID
|
||||
* String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";
|
||||
*
|
||||
* // create Instance instance
|
||||
* Instance unique = new Instance(APP_ID) {
|
||||
* @Override
|
||||
* protected void receiveMessage(String message) {
|
||||
* // print received message (timestamp)
|
||||
* System.out.println(message);
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected String sendMessage() {
|
||||
* // send timestamp as message
|
||||
* Timestamp ts = new Timestamp(new Date().getTime());
|
||||
* return "Another instance launch attempted: " + ts.toString();
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* // try to obtain lock
|
||||
* try {
|
||||
* unique.acquireLock();
|
||||
* } catch (InstanceException e) {
|
||||
* e.printStackTrace();
|
||||
* }
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* // try to free the lock before exiting program
|
||||
* try {
|
||||
* unique.freeLock();
|
||||
* } catch (InstanceException e) {
|
||||
* e.printStackTrace();
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author Pratanu Mandal
|
||||
* @since 1.3
|
||||
*
|
||||
*/
|
||||
public abstract class Instance {
|
||||
private static final Logger log = LoggerFactory.getLogger(Instance.class);
|
||||
|
||||
// starting position of port check
|
||||
private static final int PORT_START = 7221;
|
||||
|
||||
// system temporary directory path
|
||||
private static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
|
||||
|
||||
/**
|
||||
* Unique string representing the application ID.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
||||
*/
|
||||
public final String APP_ID;
|
||||
|
||||
// auto exit from application or not
|
||||
private final boolean AUTO_EXIT;
|
||||
|
||||
// lock server port
|
||||
private int port;
|
||||
|
||||
// lock server socket
|
||||
private ServerSocket server;
|
||||
|
||||
// lock file RAF object
|
||||
private RandomAccessFile lockRAF;
|
||||
|
||||
// file lock for the lock file RAF object
|
||||
private FileLock fileLock;
|
||||
|
||||
/**
|
||||
* Parameterized constructor.<br>
|
||||
* This constructor configures to automatically exit the application for subsequent instances.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
||||
*
|
||||
* @param APP_ID Unique string representing the application ID
|
||||
*/
|
||||
public Instance(final String APP_ID) {
|
||||
this(APP_ID, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameterized constructor.<br>
|
||||
* This constructor allows to explicitly specify the exit strategy for subsequent instances.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
||||
*
|
||||
* @since 1.2
|
||||
*
|
||||
* @param APP_ID Unique string representing the application ID
|
||||
* @param AUTO_EXIT If true, automatically exit the application for subsequent instances
|
||||
*/
|
||||
public Instance(final String APP_ID, final boolean AUTO_EXIT) {
|
||||
this.APP_ID = APP_ID;
|
||||
this.AUTO_EXIT = AUTO_EXIT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to obtain lock. If not possible, send data to first instance.
|
||||
*
|
||||
* @deprecated Use <code>acquireLock()</code> instead.
|
||||
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
|
||||
*/
|
||||
@Deprecated
|
||||
public void lock() throws InstanceException {
|
||||
acquireLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to obtain lock. If not possible, send data to first instance.
|
||||
*
|
||||
* @since 1.2
|
||||
*
|
||||
* @return true if able to acquire lock, false otherwise
|
||||
* @throws InstanceException throws InstanceException if it is unable to start a server or connect to server
|
||||
*/
|
||||
public boolean acquireLock() throws InstanceException {
|
||||
// try to obtain port number from lock file
|
||||
port = lockFile();
|
||||
|
||||
if (port == -1) {
|
||||
// failed to fetch port number
|
||||
// try to start server
|
||||
startServer();
|
||||
}
|
||||
else {
|
||||
// port number fetched from lock file
|
||||
// try to start client
|
||||
doClient();
|
||||
}
|
||||
|
||||
return (server != null);
|
||||
}
|
||||
|
||||
// start the server
|
||||
private void startServer() throws InstanceException {
|
||||
// try to create server
|
||||
port = PORT_START;
|
||||
while (true) {
|
||||
try {
|
||||
server = new ServerSocket(port, 50, InetAddress.getByName(null));
|
||||
break;
|
||||
} catch (IOException e) {
|
||||
port++;
|
||||
}
|
||||
}
|
||||
|
||||
// try to lock file
|
||||
lockFile(port);
|
||||
|
||||
// server created successfully; this is the first instance
|
||||
// keep listening for data from other instances
|
||||
Thread thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!server.isClosed()) {
|
||||
try {
|
||||
// establish connection
|
||||
final Socket socket = server.accept();
|
||||
|
||||
// handle socket on a different thread to allow parallel connections
|
||||
Thread thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// open writer
|
||||
OutputStream os = socket.getOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(os);
|
||||
|
||||
// open reader
|
||||
InputStream is = socket.getInputStream();
|
||||
DataInputStream dis = new DataInputStream(is);
|
||||
|
||||
// read message length from client
|
||||
int length = dis.readInt();
|
||||
|
||||
// read message string from client
|
||||
String message = null;
|
||||
if (length > -1) {
|
||||
byte[] messageBytes = new byte[length];
|
||||
int bytesRead = dis.read(messageBytes, 0, length);
|
||||
message = new String(messageBytes, 0, bytesRead, "UTF-8");
|
||||
}
|
||||
|
||||
// write response to client
|
||||
if (APP_ID == null) {
|
||||
dos.writeInt(-1);
|
||||
}
|
||||
else {
|
||||
byte[] appId = APP_ID.getBytes("UTF-8");
|
||||
|
||||
dos.writeInt(appId.length);
|
||||
dos.write(appId);
|
||||
}
|
||||
dos.flush();
|
||||
|
||||
// close writer and reader
|
||||
dos.close();
|
||||
dis.close();
|
||||
|
||||
// perform user action on message
|
||||
receiveMessage(message);
|
||||
|
||||
// close socket
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
handleException(new InstanceException(e));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// start socket thread
|
||||
thread.start();
|
||||
} catch (SocketException e) {
|
||||
if (!server.isClosed()) {
|
||||
handleException(new InstanceException(e));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
handleException(new InstanceException(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
thread.start();
|
||||
}
|
||||
|
||||
// do client tasks
|
||||
private void doClient() throws InstanceException {
|
||||
// get localhost address
|
||||
InetAddress address = null;
|
||||
try {
|
||||
address = InetAddress.getByName(null);
|
||||
} catch (UnknownHostException e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
|
||||
// try to establish connection to server
|
||||
Socket socket = null;
|
||||
try {
|
||||
socket = new Socket(address, port);
|
||||
} catch (IOException e) {
|
||||
// connection failed try to start server
|
||||
startServer();
|
||||
}
|
||||
|
||||
// connection successful try to connect to server
|
||||
if (socket != null) {
|
||||
try {
|
||||
// get message to be sent to first instance
|
||||
String message = sendMessage();
|
||||
|
||||
// open writer
|
||||
OutputStream os = socket.getOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(os);
|
||||
|
||||
// open reader
|
||||
InputStream is = socket.getInputStream();
|
||||
DataInputStream dis = new DataInputStream(is);
|
||||
|
||||
// write message to server
|
||||
if (message == null) {
|
||||
dos.writeInt(-1);
|
||||
}
|
||||
else {
|
||||
byte[] messageBytes = message.getBytes("UTF-8");
|
||||
|
||||
dos.writeInt(messageBytes.length);
|
||||
dos.write(messageBytes);
|
||||
}
|
||||
|
||||
dos.flush();
|
||||
|
||||
// read response length from server
|
||||
int length = dis.readInt();
|
||||
|
||||
// read response string from server
|
||||
String response = null;
|
||||
if (length > -1) {
|
||||
byte[] responseBytes = new byte[length];
|
||||
int bytesRead = dis.read(responseBytes, 0, length);
|
||||
response = new String(responseBytes, 0, bytesRead, "UTF-8");
|
||||
}
|
||||
|
||||
// close writer and reader
|
||||
dos.close();
|
||||
dis.close();
|
||||
|
||||
if (response.equals(APP_ID)) {
|
||||
// validation successful
|
||||
if (AUTO_EXIT) {
|
||||
// perform pre-exit tasks
|
||||
beforeExit();
|
||||
// exit this instance
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// validation failed, this is the first instance
|
||||
startServer();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
} finally {
|
||||
// close socket
|
||||
try {
|
||||
if (socket != null) socket.close();
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// try to get port from lock file
|
||||
private int lockFile() throws InstanceException {
|
||||
// lock file path
|
||||
String filePath = TEMP_DIR + File.separator + APP_ID + ".lock";
|
||||
File file = new File(filePath);
|
||||
|
||||
// try to get port from lock file
|
||||
if (file.exists()) {
|
||||
BufferedReader br = null;
|
||||
try {
|
||||
br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
|
||||
return Integer.parseInt(br.readLine());
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
} catch (NumberFormatException e) {
|
||||
// do nothing
|
||||
} finally {
|
||||
try {
|
||||
if (br != null) br.close();
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// try to write port to lock file
|
||||
private void lockFile(int port) throws InstanceException {
|
||||
// lock file path
|
||||
String filePath = TEMP_DIR + File.separator + APP_ID + ".lock";
|
||||
File file = new File(filePath);
|
||||
|
||||
// try to write port to lock file
|
||||
BufferedWriter bw = null;
|
||||
try {
|
||||
bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));
|
||||
bw.write(String.valueOf(port));
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
} finally {
|
||||
try {
|
||||
if (bw != null) bw.close();
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
// try to obtain file lock
|
||||
try {
|
||||
lockRAF = new RandomAccessFile(file, "rw");
|
||||
FileChannel fc = lockRAF.getChannel();
|
||||
fileLock = fc.tryLock(0, Long.MAX_VALUE, true);
|
||||
if (fileLock == null) {
|
||||
throw new InstanceException("Failed to obtain file lock");
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new InstanceException(e);
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the lock if possible. This is only required to be called from the first instance.
|
||||
*
|
||||
* @deprecated Use <code>freeLock()</code> instead.
|
||||
* @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock
|
||||
*/
|
||||
@Deprecated
|
||||
public void free() throws InstanceException {
|
||||
freeLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the lock if possible. This is only required to be called from the first instance.
|
||||
*
|
||||
* @since 1.2
|
||||
*
|
||||
* @return true if able to release lock, false otherwise
|
||||
* @throws InstanceException throws InstanceException if it is unable to stop the server or release file lock
|
||||
*/
|
||||
public boolean freeLock() throws InstanceException {
|
||||
try {
|
||||
// close server socket
|
||||
if (server != null) {
|
||||
server.close();
|
||||
|
||||
// lock file path
|
||||
String filePath = TEMP_DIR + File.separator + APP_ID + ".lock";
|
||||
File file = new File(filePath);
|
||||
|
||||
// try to release file lock
|
||||
if (fileLock != null) {
|
||||
fileLock.release();
|
||||
}
|
||||
|
||||
// try to close lock file RAF object
|
||||
if (lockRAF != null) {
|
||||
lockRAF.close();
|
||||
}
|
||||
|
||||
// try to delete lock file
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
throw new InstanceException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method used in first instance to receive messages from subsequent instances.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @param message message received by first instance from subsequent instances
|
||||
*/
|
||||
protected abstract void receiveMessage(String message);
|
||||
|
||||
/**
|
||||
* Method used in subsequent instances to send message to first instance.<br><br>
|
||||
*
|
||||
* It is not recommended to perform blocking (long running) tasks here. Use <code>beforeExit()</code> method instead.<br>
|
||||
* One exception to this rule is if you intend to perform some user interaction before sending the message.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @return message sent from subsequent instances
|
||||
*/
|
||||
protected abstract String sendMessage();
|
||||
|
||||
/**
|
||||
* Method to receive and handle exceptions occurring while first instance is listening for subsequent instances.<br><br>
|
||||
*
|
||||
* By default prints stack trace of all exceptions. Override this method to handle exceptions explicitly.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @param exception exception occurring while first instance is listening for subsequent instances
|
||||
*/
|
||||
protected void handleException(Exception exception) {
|
||||
log.error("Error listening for instances", exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called before exiting from subsequent instances.<br><br>
|
||||
*
|
||||
* Override this method to perform blocking tasks before exiting from subsequent instances.<br>
|
||||
* This method is not invoked if auto exit is turned off.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @since 1.2
|
||||
*/
|
||||
protected void beforeExit() {}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright 2019 Pratanu Mandal
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package com.sparrowwallet.sparrow.instance;
|
||||
|
||||
/**
|
||||
* The <code>InstanceException</code> class is a wrapper for all exceptions thrown from Instance.
|
||||
*
|
||||
* @author Pratanu Mandal
|
||||
* @since 1.1
|
||||
*
|
||||
*/
|
||||
public class InstanceException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 268060627071973613L;
|
||||
|
||||
/**
|
||||
* Constructs a new exception with null as its detail message.
|
||||
*/
|
||||
public InstanceException() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new exception with the specified detail message.<br>
|
||||
* The cause is not initialized, and may subsequently be initialized by a call to {@link #initCause}.
|
||||
*
|
||||
* @param message the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
|
||||
*/
|
||||
public InstanceException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new exception with the specified detail message and cause.<br><br>
|
||||
* Note that the detail message associated with cause is not automatically incorporated in this exception's detail message.
|
||||
*
|
||||
* @param message the detail message (which is saved for later retrieval by the {@link #getMessage()} method).
|
||||
* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). (A null value is permitted, and indicates that the cause is nonexistent or unknown.)
|
||||
*/
|
||||
public InstanceException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new exception with the specified cause and a detail message of (cause==null ? null : cause.toString()) (which typically contains the class and detail message of cause).<br>
|
||||
* This constructor is useful for exceptions that are little more than wrappers for other throwables (for example, {@link java.security.PrivilegedActionException}).
|
||||
*
|
||||
* @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method). (A null value is permitted, and indicates that the cause is nonexistent or unknown.)
|
||||
*/
|
||||
public InstanceException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package com.sparrowwallet.sparrow.instance;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
/**
|
||||
* The <code>InstanceList</code> class is a logical entry point to the library which extends the functionality of the <code>Instance</code> class.<br>
|
||||
* It allows to create an application lock or free it and send and receive messages between first and subsequent instances.<br><br>
|
||||
*
|
||||
* This class is intended for passing a list of strings instead of a single string from the subsequent instance to the first instance.<br><br>
|
||||
*
|
||||
* <pre>
|
||||
* // unique application ID
|
||||
* String APP_ID = "tk.pratanumandal.unique4j-mlsdvo-20191511-#j.6";
|
||||
*
|
||||
* // create Instance instance
|
||||
* Instance unique = new InstanceList(APP_ID) {
|
||||
* @Override
|
||||
* protected List<String> sendMessageList() {
|
||||
* List<String> messageList = new ArrayList<String>();
|
||||
*
|
||||
* messageList.add("Message 1");
|
||||
* messageList.add("Message 2");
|
||||
* messageList.add("Message 3");
|
||||
* messageList.add("Message 4");
|
||||
*
|
||||
* return messageList;
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* protected void receiveMessageList(List<String> messageList) {
|
||||
* for (String message : messageList) {
|
||||
* System.out.println(message);
|
||||
* }
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* // try to obtain lock
|
||||
* try {
|
||||
* unique.acquireLock();
|
||||
* } catch (InstanceException e) {
|
||||
* e.printStackTrace();
|
||||
* }
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* // try to free the lock before exiting program
|
||||
* try {
|
||||
* unique.freeLock();
|
||||
* } catch (InstanceException e) {
|
||||
* e.printStackTrace();
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author Pratanu Mandal
|
||||
* @since 1.3
|
||||
*
|
||||
*/
|
||||
public abstract class InstanceList extends Instance {
|
||||
|
||||
/**
|
||||
* Parameterized constructor.<br>
|
||||
* This constructor configures to automatically exit the application for subsequent instances.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
||||
*
|
||||
* @param APP_ID Unique string representing the application ID
|
||||
*/
|
||||
public InstanceList(String APP_ID) {
|
||||
super(APP_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameterized constructor.<br>
|
||||
* This constructor allows to explicitly specify the exit strategy for subsequent instances.<br><br>
|
||||
*
|
||||
* The APP_ID must be as unique as possible.
|
||||
* Avoid generic names like "my_app_id" or "hello_world".<br>
|
||||
* A good strategy is to use the entire package name (group ID + artifact ID) along with some random characters.
|
||||
*
|
||||
* @param APP_ID Unique string representing the application ID
|
||||
* @param AUTO_EXIT If true, automatically exit the application for subsequent instances
|
||||
*/
|
||||
public InstanceList(String APP_ID, boolean AUTO_EXIT) {
|
||||
super(APP_ID, AUTO_EXIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method used in first instance to receive and parse messages from subsequent instances.<br>
|
||||
* The use of this method directly in <code>InstanceList</code> is discouraged. Use <code>receiveMessageList()</code> instead.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @param message message received by first instance from subsequent instances
|
||||
*/
|
||||
@Override
|
||||
protected final void receiveMessage(String message) {
|
||||
if (message == null) {
|
||||
receiveMessageList(null);
|
||||
}
|
||||
else {
|
||||
// parse the JSON array string into an array of string arguments
|
||||
JsonArray jsonArgs = JsonParser.parseString(message).getAsJsonArray();
|
||||
|
||||
List<String> stringArgs = new ArrayList<String>(jsonArgs.size());
|
||||
|
||||
for (int i = 0; i < jsonArgs.size(); i++) {
|
||||
JsonElement element = jsonArgs.get(i);
|
||||
stringArgs.add(element.getAsString());
|
||||
}
|
||||
|
||||
// return the parsed string list
|
||||
receiveMessageList(stringArgs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method used in subsequent instances to parse and send message to first instance.<br>
|
||||
* The use of this method directly in <code>InstanceList</code> is discouraged. Use <code>sendMessageList()</code> instead.<br><br>
|
||||
*
|
||||
* It is not recommended to perform blocking (long running) tasks here. Use <code>beforeExit()</code> method instead.<br>
|
||||
* One exception to this rule is if you intend to perform some user interaction before sending the message.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @return message sent from subsequent instances
|
||||
*/
|
||||
@Override
|
||||
protected final String sendMessage() {
|
||||
// convert arguments to JSON array string
|
||||
JsonArray jsonArgs = new JsonArray();
|
||||
|
||||
List<String> stringArgs = sendMessageList();
|
||||
|
||||
if (stringArgs == null) return null;
|
||||
|
||||
for (String arg : stringArgs) {
|
||||
jsonArgs.add(arg);
|
||||
}
|
||||
|
||||
// return the JSON array string
|
||||
return jsonArgs.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method used in first instance to receive list of messages from subsequent instances.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @param messageList list of messages received by first instance from subsequent instances
|
||||
*/
|
||||
protected abstract void receiveMessageList(List<String> messageList);
|
||||
|
||||
/**
|
||||
* Method used in subsequent instances to send list of messages to first instance.<br><br>
|
||||
*
|
||||
* It is not recommended to perform blocking (long running) tasks here. Use <code>beforeExit()</code> method instead.<br>
|
||||
* One exception to this rule is if you intend to perform some user interaction before sending the message.<br><br>
|
||||
*
|
||||
* This method is not synchronized.
|
||||
*
|
||||
* @return list of messages sent from subsequent instances
|
||||
*/
|
||||
protected abstract List<String> sendMessageList();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -57,8 +57,8 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
|
||||
return keystore;
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting Cobo Vault keystore", e);
|
||||
throw new ImportException("Error getting Cobo Vault keystore", e);
|
||||
log.error("Error getting " + getName() + " keystore", e);
|
||||
throw new ImportException("Error getting " + getName() + " keystore", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
try {
|
||||
wallet.checkWallet();
|
||||
} catch(InvalidWalletException e) {
|
||||
throw new ImportException("Imported Cobo Vault wallet was invalid: " + e.getMessage());
|
||||
throw new ImportException("Imported " + getName() + " wallet was invalid: " + e.getMessage());
|
||||
}
|
||||
|
||||
return wallet;
|
||||
|
||||
@@ -36,7 +36,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||
ColdcardKeystore cck = Storage.getGson().fromJson(reader, ColdcardKeystore.class);
|
||||
ColdcardKeystore cck = JsonPersistence.getGson().fromJson(reader, ColdcardKeystore.class);
|
||||
|
||||
Keystore keystore = new Keystore("Coldcard");
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
@@ -145,13 +145,13 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
wallet.setScriptType(scriptType);
|
||||
|
||||
if(!wallet.isValid()) {
|
||||
throw new IllegalStateException("This file does not describe a valid wallet. Please use the Settings > Multisig Wallets > Export XPUB feature on your Coldcard.");
|
||||
throw new IllegalStateException("This file does not describe a valid wallet. " + getKeystoreImportDescription());
|
||||
}
|
||||
|
||||
return wallet;
|
||||
} catch(Exception e) {
|
||||
log.error("Error importing Coldcard multisig wallet", e);
|
||||
throw new ImportException("Error importing Coldcard multisig wallet", e);
|
||||
log.error("Error importing " + getName() + " wallet", e);
|
||||
throw new ImportException("Error importing " + getName() + " wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
}
|
||||
|
||||
try {
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
writer.append("# " + getName() + " setup file (created by Sparrow)\n");
|
||||
writer.append("#\n");
|
||||
writer.append("Name: ").append(wallet.getName()).append("\n");
|
||||
@@ -202,10 +202,9 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
}
|
||||
|
||||
writer.flush();
|
||||
writer.close();
|
||||
} catch(Exception e) {
|
||||
log.error("Error exporting Coldcard multisig wallet", e);
|
||||
throw new ExportException("Error exporting Coldcard multisig wallet", e);
|
||||
log.error("Error exporting " + getName() + " wallet", e);
|
||||
throw new ExportException("Error exporting " + getName() + " wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting Coldcard keystore", e);
|
||||
throw new ImportException("Error getting Coldcard keystore", e);
|
||||
log.error("Error getting " + getName() + " keystore", e);
|
||||
throw new ImportException("Error getting " + getName() + " keystore", e);
|
||||
}
|
||||
|
||||
throw new ImportException("Correct derivation not found for script type: " + scriptType);
|
||||
|
||||
@@ -11,6 +11,8 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.Arrays;
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
@@ -38,10 +40,13 @@ public class Config {
|
||||
private boolean openWalletsInNewWindows = false;
|
||||
private boolean hideEmptyUsedAddresses = false;
|
||||
private boolean showTransactionHex = true;
|
||||
private boolean showLoadingLog = false;
|
||||
private boolean showUtxosChart = true;
|
||||
private List<File> recentWalletFiles;
|
||||
private Integer keyDerivationPeriod;
|
||||
private File hwi;
|
||||
private Boolean hdCapture;
|
||||
private String webcamDevice;
|
||||
private ServerType serverType;
|
||||
private String publicElectrumServer;
|
||||
private String coreServer;
|
||||
@@ -66,10 +71,6 @@ public class Config {
|
||||
|
||||
private static File getConfigFile() {
|
||||
File sparrowDir = Storage.getSparrowDir();
|
||||
if(!sparrowDir.exists()) {
|
||||
sparrowDir.mkdirs();
|
||||
}
|
||||
|
||||
return new File(sparrowDir, CONFIG_FILENAME);
|
||||
}
|
||||
|
||||
@@ -249,6 +250,24 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isShowLoadingLog() {
|
||||
return showLoadingLog;
|
||||
}
|
||||
|
||||
public void setShowLoadingLog(boolean showLoadingLog) {
|
||||
this.showLoadingLog = showLoadingLog;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isShowUtxosChart() {
|
||||
return showUtxosChart;
|
||||
}
|
||||
|
||||
public void setShowUtxosChart(boolean showUtxosChart) {
|
||||
this.showUtxosChart = showUtxosChart;
|
||||
flush();
|
||||
}
|
||||
|
||||
public List<File> getRecentWalletFiles() {
|
||||
return recentWalletFiles;
|
||||
}
|
||||
@@ -289,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;
|
||||
}
|
||||
@@ -435,6 +463,10 @@ public class Config {
|
||||
Gson gson = getGson();
|
||||
try {
|
||||
File configFile = getConfigFile();
|
||||
if(!configFile.exists()) {
|
||||
Storage.createOwnerOnlyFile(configFile);
|
||||
}
|
||||
|
||||
Writer writer = new FileWriter(configFile);
|
||||
gson.toJson(this, writer);
|
||||
writer.flush();
|
||||
|
||||
@@ -367,7 +367,6 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
String json = gson.toJson(eJson);
|
||||
outputStream.write(json.getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
} catch (Exception e) {
|
||||
log.error("Error exporting Electrum Wallet", e);
|
||||
throw new ExportException("Error exporting Electrum Wallet", e);
|
||||
|
||||
@@ -30,7 +30,10 @@ import java.util.zip.ZipInputStream;
|
||||
|
||||
public class Hwi {
|
||||
private static final Logger log = LoggerFactory.getLogger(Hwi.class);
|
||||
private static final String VERSION_PREFIX = "hwi-2.0.1";
|
||||
private static final String HWI_HOME_DIR = "hwi";
|
||||
private static final String HWI_VERSION_PREFIX = "hwi-";
|
||||
private static final String HWI_VERSION = "2.0.2";
|
||||
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
|
||||
|
||||
private static boolean isPromptActive = false;
|
||||
|
||||
@@ -51,7 +54,7 @@ public class Hwi {
|
||||
}
|
||||
return Arrays.stream(devices).filter(device -> device != null && device.getModel() != null).collect(Collectors.toList());
|
||||
} catch(IOException e) {
|
||||
log.error("Error executing " + VERSION_PREFIX, e);
|
||||
log.error("Error executing " + HWI_VERSION_DIR, e);
|
||||
throw new ImportException(e);
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
@@ -116,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();
|
||||
@@ -222,11 +225,12 @@ public class Hwi {
|
||||
private synchronized File getHwiExecutable(Command command) {
|
||||
File hwiExecutable = Config.get().getHwi();
|
||||
if(hwiExecutable != null && hwiExecutable.exists()) {
|
||||
File homeDir = getHwiHomeDir();
|
||||
String tmpDir = System.getProperty("java.io.tmpdir");
|
||||
String hwiPath = hwiExecutable.getAbsolutePath();
|
||||
if(command.isTestFirst() && hwiPath.startsWith(tmpDir) && (!hwiPath.contains(VERSION_PREFIX) || !testHwi(hwiExecutable))) {
|
||||
if(command.isTestFirst() && (hwiPath.contains(tmpDir) || hwiPath.startsWith(homeDir.getAbsolutePath())) && (!hwiPath.contains(HWI_VERSION_DIR) || !testHwi(hwiExecutable))) {
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
deleteDirectory(hwiExecutable.getParentFile());
|
||||
IOUtils.deleteDirectory(hwiExecutable.getParentFile());
|
||||
} else {
|
||||
hwiExecutable.delete();
|
||||
}
|
||||
@@ -243,55 +247,66 @@ public class Hwi {
|
||||
//The check will still happen on first invocation, but will not thereafter
|
||||
//See https://github.com/bitcoin-core/HWI/issues/327 for details
|
||||
if(platform == Platform.OSX) {
|
||||
InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + VERSION_PREFIX + "-mac-amd64-signed.zip");
|
||||
Path tempHwiDirPath = Files.createTempDirectory(VERSION_PREFIX, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
File tempHwiDir = tempHwiDirPath.toFile();
|
||||
//tempHwiDir.deleteOnExit();
|
||||
log.debug("Using temp HWI path: " + tempHwiDir.getAbsolutePath());
|
||||
|
||||
File tempExec = null;
|
||||
ZipInputStream zis = new ZipInputStream(inputStream);
|
||||
ZipEntry zipEntry = zis.getNextEntry();
|
||||
while(zipEntry != null) {
|
||||
if(zipEntry.isDirectory()) {
|
||||
newDirectory(tempHwiDir, zipEntry, ownerExecutableWritable);
|
||||
} else {
|
||||
File newFile = newFile(tempHwiDir, zipEntry, ownerExecutableWritable);
|
||||
//newFile.deleteOnExit();
|
||||
FileOutputStream fos = new FileOutputStream(newFile);
|
||||
ByteStreams.copy(zis, new FileOutputStream(newFile));
|
||||
fos.flush();
|
||||
fos.close();
|
||||
|
||||
if(zipEntry.getName().equals("hwi")) {
|
||||
tempExec = newFile;
|
||||
}
|
||||
}
|
||||
|
||||
zipEntry = zis.getNextEntry();
|
||||
InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.zip");
|
||||
if(inputStream == null) {
|
||||
throw new IllegalStateException("Cannot load " + HWI_VERSION_DIR + " from classpath");
|
||||
}
|
||||
zis.closeEntry();
|
||||
zis.close();
|
||||
|
||||
hwiExecutable = tempExec;
|
||||
File hwiHomeDir = getHwiHomeDir();
|
||||
File hwiVersionDir = new File(hwiHomeDir, HWI_VERSION_DIR);
|
||||
IOUtils.deleteDirectory(hwiVersionDir);
|
||||
if(!hwiVersionDir.exists()) {
|
||||
Files.createDirectories(hwiVersionDir.toPath(), PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
}
|
||||
|
||||
log.debug("Using HWI path: " + hwiVersionDir.getAbsolutePath());
|
||||
|
||||
File hwiExec = null;
|
||||
try(ZipInputStream zis = new ZipInputStream(inputStream)) {
|
||||
ZipEntry zipEntry = zis.getNextEntry();
|
||||
while(zipEntry != null) {
|
||||
if(zipEntry.isDirectory()) {
|
||||
newDirectory(hwiVersionDir, zipEntry, ownerExecutableWritable);
|
||||
} else {
|
||||
File newFile = newFile(hwiVersionDir, zipEntry, ownerExecutableWritable);
|
||||
try(FileOutputStream fos = new FileOutputStream(newFile)) {
|
||||
ByteStreams.copy(zis, new FileOutputStream(newFile));
|
||||
fos.flush();
|
||||
};
|
||||
|
||||
if(zipEntry.getName().equals("hwi")) {
|
||||
hwiExec = newFile;
|
||||
}
|
||||
}
|
||||
|
||||
zipEntry = zis.getNextEntry();
|
||||
}
|
||||
zis.closeEntry();
|
||||
}
|
||||
|
||||
hwiExecutable = hwiExec;
|
||||
} else {
|
||||
InputStream inputStream;
|
||||
Path tempExecPath;
|
||||
if(platform == Platform.WINDOWS) {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
|
||||
tempExecPath = Files.createTempFile(VERSION_PREFIX, null);
|
||||
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null);
|
||||
} else {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
|
||||
tempExecPath = Files.createTempFile(VERSION_PREFIX, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
}
|
||||
|
||||
if(inputStream == null) {
|
||||
throw new IllegalStateException("Cannot load " + HWI_VERSION_DIR + " from classpath");
|
||||
}
|
||||
|
||||
File tempExec = tempExecPath.toFile();
|
||||
tempExec.deleteOnExit();
|
||||
OutputStream tempExecStream = new BufferedOutputStream(new FileOutputStream(tempExec));
|
||||
ByteStreams.copy(inputStream, tempExecStream);
|
||||
inputStream.close();
|
||||
tempExecStream.flush();
|
||||
tempExecStream.close();
|
||||
try(OutputStream tempExecStream = new BufferedOutputStream(new FileOutputStream(tempExec))) {
|
||||
ByteStreams.copy(inputStream, tempExecStream);
|
||||
inputStream.close();
|
||||
tempExecStream.flush();
|
||||
};
|
||||
|
||||
hwiExecutable = tempExec;
|
||||
}
|
||||
@@ -305,6 +320,14 @@ public class Hwi {
|
||||
return hwiExecutable;
|
||||
}
|
||||
|
||||
private File getHwiHomeDir() {
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
return new File(Storage.getSparrowDir(), HWI_HOME_DIR);
|
||||
}
|
||||
|
||||
return new File(System.getProperty("java.io.tmpdir"));
|
||||
}
|
||||
|
||||
private boolean testHwi(File hwiExecutable) {
|
||||
try {
|
||||
List<String> command = List.of(hwiExecutable.getAbsolutePath(), "--version");
|
||||
@@ -317,17 +340,6 @@ public class Hwi {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean deleteDirectory(File directoryToBeDeleted) {
|
||||
File[] allContents = directoryToBeDeleted.listFiles();
|
||||
if (allContents != null) {
|
||||
for (File file : allContents) {
|
||||
deleteDirectory(file);
|
||||
}
|
||||
}
|
||||
|
||||
return directoryToBeDeleted.delete();
|
||||
}
|
||||
|
||||
public static File newDirectory(File destinationDir, ZipEntry zipEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
String destDirPath = destinationDir.getCanonicalPath();
|
||||
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
public class IOUtils {
|
||||
public static FileType getFileType(File file) {
|
||||
@@ -16,10 +25,12 @@ public class IOUtils {
|
||||
if(file.exists()) {
|
||||
try(BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8))) {
|
||||
String line = br.readLine();
|
||||
if(line.startsWith("01000000") || line.startsWith("cHNid")) {
|
||||
return FileType.TEXT;
|
||||
} else if(line.startsWith("{")) {
|
||||
return FileType.JSON;
|
||||
if(line != null) {
|
||||
if(line.startsWith("01000000") || line.startsWith("cHNid")) {
|
||||
return FileType.TEXT;
|
||||
} else if(line.startsWith("{")) {
|
||||
return FileType.JSON;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,4 +47,77 @@ public class IOUtils {
|
||||
|
||||
return FileType.UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* List directory contents for a resource folder. Not recursive.
|
||||
* This is basically a brute-force implementation.
|
||||
* Works for regular files, JARs and Java modules.
|
||||
*
|
||||
* @param clazz Any java class that lives in the same place as the resources you want.
|
||||
* @param path Should end with "/", but not start with one.
|
||||
* @return Just the name of each member item, not the full paths.
|
||||
* @throws URISyntaxException
|
||||
* @throws IOException
|
||||
*/
|
||||
public static String[] getResourceListing(Class clazz, String path) throws URISyntaxException, IOException {
|
||||
URL dirURL = clazz.getClassLoader().getResource(path);
|
||||
if(dirURL != null && dirURL.getProtocol().equals("file")) {
|
||||
/* A file path: easy enough */
|
||||
return new File(dirURL.toURI()).list();
|
||||
}
|
||||
|
||||
if(dirURL == null) {
|
||||
/*
|
||||
* In case of a jar file, we can't actually find a directory.
|
||||
* Have to assume the same jar as clazz.
|
||||
*/
|
||||
String me = clazz.getName().replace(".", "/")+".class";
|
||||
dirURL = clazz.getClassLoader().getResource(me);
|
||||
}
|
||||
|
||||
if(dirURL.getProtocol().equals("jar")) {
|
||||
/* A JAR path */
|
||||
String jarPath = dirURL.getPath().substring(5, dirURL.getPath().indexOf("!")); //strip out only the JAR file
|
||||
JarFile jar = new JarFile(URLDecoder.decode(jarPath, "UTF-8"));
|
||||
Enumeration<JarEntry> entries = jar.entries(); //gives ALL entries in jar
|
||||
Set<String> result = new HashSet<String>(); //avoid duplicates in case it is a subdirectory
|
||||
while(entries.hasMoreElements()) {
|
||||
String name = entries.nextElement().getName();
|
||||
if(name.startsWith(path)) { //filter according to the path
|
||||
String entry = name.substring(path.length());
|
||||
int checkSubdir = entry.indexOf("/");
|
||||
if (checkSubdir >= 0) {
|
||||
// if it is a subdirectory, we just return the directory name
|
||||
entry = entry.substring(0, checkSubdir);
|
||||
}
|
||||
if(!entry.isEmpty()) {
|
||||
result.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.toArray(new String[result.size()]);
|
||||
}
|
||||
|
||||
if(dirURL.getProtocol().equals("jrt")) {
|
||||
java.nio.file.FileSystem jrtFs = FileSystems.newFileSystem(URI.create("jrt:/"), Collections.emptyMap());
|
||||
Path resourcePath = jrtFs.getPath("modules/com.sparrowwallet.sparrow", path);
|
||||
return Files.list(resourcePath).map(filePath -> filePath.getFileName().toString()).toArray(String[]::new);
|
||||
}
|
||||
|
||||
throw new UnsupportedOperationException("Cannot list files for URL " + dirURL);
|
||||
}
|
||||
|
||||
public static boolean deleteDirectory(File directory) {
|
||||
try {
|
||||
Files.walk(directory.toPath())
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.map(Path::toFile)
|
||||
.forEach(File::delete);
|
||||
} catch(IOException e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,484 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.Argon2KeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
import static com.sparrowwallet.drongo.crypto.Argon2KeyDeriver.SPRW1_PARAMETERS;
|
||||
|
||||
public class JsonPersistence implements Persistence {
|
||||
public static final String HEADER_MAGIC_1 = "SPRW1";
|
||||
public static final int BINARY_HEADER_LENGTH = 28;
|
||||
|
||||
private final Gson gson;
|
||||
private AsymmetricKeyDeriver keyDeriver;
|
||||
|
||||
public JsonPersistence() {
|
||||
this.gson = getGson();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException {
|
||||
Wallet wallet;
|
||||
|
||||
try(Reader reader = new FileReader(storage.getWalletFile())) {
|
||||
wallet = gson.fromJson(reader, Wallet.class);
|
||||
}
|
||||
|
||||
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, wallet, null);
|
||||
wallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
File backupFile = storage.getTempBackup();
|
||||
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, null);
|
||||
|
||||
return new WalletBackupAndKey(wallet, backupWallet, null, null, childWallets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletBackupAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException {
|
||||
return loadWallet(storage, password, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletBackupAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException {
|
||||
Wallet wallet;
|
||||
ECKey encryptionKey;
|
||||
|
||||
try(InputStream fileStream = new FileInputStream(storage.getWalletFile())) {
|
||||
encryptionKey = getEncryptionKey(password, fileStream, alreadyDerivedKey);
|
||||
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||
wallet = gson.fromJson(reader, Wallet.class);
|
||||
}
|
||||
|
||||
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, wallet, encryptionKey);
|
||||
wallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
File backupFile = storage.getTempBackup();
|
||||
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, encryptionKey);
|
||||
|
||||
return new WalletBackupAndKey(wallet, backupWallet, encryptionKey, keyDeriver, childWallets);
|
||||
}
|
||||
|
||||
private Map<Storage, WalletBackupAndKey> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
|
||||
File[] walletFiles = getChildWalletFiles(storage.getWalletFile(), masterWallet);
|
||||
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
|
||||
for(File childFile : walletFiles) {
|
||||
Wallet childWallet = loadWallet(childFile, encryptionKey);
|
||||
Storage childStorage = new Storage(childFile);
|
||||
childStorage.setEncryptionPubKey(encryptionKey == null ? Storage.NO_PASSWORD_KEY : ECKey.fromPublicOnly(encryptionKey));
|
||||
childStorage.setKeyDeriver(getKeyDeriver());
|
||||
childWallet.setMasterWallet(masterWallet);
|
||||
childWallets.put(childStorage, new WalletBackupAndKey(childWallet, null, encryptionKey, keyDeriver, Collections.emptyMap()));
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
private Wallet loadWallet(File walletFile, ECKey encryptionKey) throws IOException, StorageException {
|
||||
if(encryptionKey != null) {
|
||||
try(InputStream fileStream = new FileInputStream(walletFile)) {
|
||||
encryptionKey = getEncryptionKey(null, fileStream, encryptionKey);
|
||||
Reader reader = new InputStreamReader(new InflaterInputStream(new ECIESInputStream(fileStream, encryptionKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||
return gson.fromJson(reader, Wallet.class);
|
||||
}
|
||||
} else {
|
||||
try(Reader reader = new FileReader(walletFile)) {
|
||||
return gson.fromJson(reader, Wallet.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private File[] getChildWalletFiles(File walletFile, Wallet masterWallet) {
|
||||
File childDir = new File(walletFile.getParentFile(), masterWallet.getName() + "-child");
|
||||
if(!childDir.exists()) {
|
||||
return new File[0];
|
||||
}
|
||||
|
||||
File[] childFiles = childDir.listFiles(pathname -> {
|
||||
FileType fileType = IOUtils.getFileType(pathname);
|
||||
return pathname.getName().startsWith(masterWallet.getName()) && (fileType == FileType.BINARY || fileType == FileType.JSON);
|
||||
});
|
||||
|
||||
return childFiles == null ? new File[0] : childFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File storeWallet(Storage storage, Wallet wallet) throws IOException {
|
||||
File walletFile = storage.getWalletFile();
|
||||
|
||||
if(!walletFile.getName().endsWith(".json")) {
|
||||
File jsonFile = new File(walletFile.getParentFile(), walletFile.getName() + ".json");
|
||||
if(walletFile.exists()) {
|
||||
if(!walletFile.renameTo(jsonFile)) {
|
||||
throw new IOException("Could not rename " + walletFile.getName() + " to " + jsonFile.getName());
|
||||
}
|
||||
}
|
||||
walletFile = jsonFile;
|
||||
}
|
||||
|
||||
if(!walletFile.exists()) {
|
||||
Storage.createOwnerOnlyFile(walletFile);
|
||||
}
|
||||
|
||||
try(Writer writer = new FileWriter(walletFile)) {
|
||||
gson.toJson(wallet, writer);
|
||||
}
|
||||
|
||||
return walletFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File storeWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException {
|
||||
File walletFile = storage.getWalletFile();
|
||||
|
||||
if(walletFile.getName().endsWith(".json")) {
|
||||
File noJsonFile = new File(walletFile.getParentFile(), walletFile.getName().substring(0, walletFile.getName().lastIndexOf('.')));
|
||||
if(walletFile.exists()) {
|
||||
if(!walletFile.renameTo(noJsonFile)) {
|
||||
throw new IOException("Could not rename " + walletFile.getName() + " to " + noJsonFile.getName());
|
||||
}
|
||||
}
|
||||
walletFile = noJsonFile;
|
||||
}
|
||||
|
||||
if(!walletFile.exists()) {
|
||||
Storage.createOwnerOnlyFile(walletFile);
|
||||
}
|
||||
|
||||
try(OutputStream outputStream = new FileOutputStream(walletFile)) {
|
||||
writeBinaryHeader(outputStream);
|
||||
OutputStreamWriter writer = new OutputStreamWriter(new DeflaterOutputStream(new ECIESOutputStream(outputStream, encryptionPubKey, getEncryptionMagic())), StandardCharsets.UTF_8);
|
||||
gson.toJson(wallet, writer);
|
||||
//Close the writer explicitly as the try-resources block will not do so
|
||||
writer.close();
|
||||
}
|
||||
|
||||
return walletFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateWallet(Storage storage, Wallet wallet) throws IOException {
|
||||
storeWallet(storage, wallet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException {
|
||||
storeWallet(storage, wallet, encryptionPubKey);
|
||||
}
|
||||
|
||||
private void writeBinaryHeader(OutputStream outputStream) throws IOException {
|
||||
ByteBuffer buf = ByteBuffer.allocate(21);
|
||||
buf.put(HEADER_MAGIC_1.getBytes(StandardCharsets.UTF_8));
|
||||
buf.put(keyDeriver.getSalt());
|
||||
|
||||
byte[] encoded = Base64.getEncoder().encode(buf.array());
|
||||
if(encoded.length != BINARY_HEADER_LENGTH) {
|
||||
throw new IllegalStateException("Header length not " + BINARY_HEADER_LENGTH + " bytes");
|
||||
}
|
||||
outputStream.write(encoded);
|
||||
}
|
||||
|
||||
private static byte[] getEncryptionMagic() {
|
||||
return "BIE1".getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException {
|
||||
return getEncryptionKey(password, null, null);
|
||||
}
|
||||
|
||||
private ECKey getEncryptionKey(CharSequence password, InputStream inputStream, ECKey alreadyDerivedKey) throws IOException, StorageException {
|
||||
if(password != null && password.equals("")) {
|
||||
return Storage.NO_PASSWORD_KEY;
|
||||
}
|
||||
|
||||
AsymmetricKeyDeriver keyDeriver = getKeyDeriver(inputStream);
|
||||
return alreadyDerivedKey == null ? keyDeriver.deriveECKey(password) : alreadyDerivedKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AsymmetricKeyDeriver getKeyDeriver() {
|
||||
return keyDeriver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setKeyDeriver(AsymmetricKeyDeriver keyDeriver) {
|
||||
this.keyDeriver = keyDeriver;
|
||||
}
|
||||
|
||||
private AsymmetricKeyDeriver getKeyDeriver(InputStream inputStream) throws IOException, StorageException {
|
||||
if(keyDeriver == null) {
|
||||
keyDeriver = getWalletKeyDeriver(inputStream);
|
||||
} else if(inputStream != null) {
|
||||
inputStream.skip(BINARY_HEADER_LENGTH);
|
||||
}
|
||||
|
||||
return keyDeriver;
|
||||
}
|
||||
|
||||
private AsymmetricKeyDeriver getWalletKeyDeriver(InputStream inputStream) throws IOException, StorageException {
|
||||
byte[] salt = new byte[SPRW1_PARAMETERS.saltLength];
|
||||
|
||||
if(inputStream != null) {
|
||||
byte[] header = new byte[BINARY_HEADER_LENGTH];
|
||||
int read = inputStream.read(header);
|
||||
if(read != BINARY_HEADER_LENGTH) {
|
||||
throw new StorageException("Not a Sparrow wallet - invalid header");
|
||||
}
|
||||
try {
|
||||
byte[] decodedHeader = Base64.getDecoder().decode(header);
|
||||
byte[] magic = Arrays.copyOfRange(decodedHeader, 0, HEADER_MAGIC_1.length());
|
||||
if(!HEADER_MAGIC_1.equals(new String(magic, StandardCharsets.UTF_8))) {
|
||||
throw new StorageException("Not a Sparrow wallet - invalid magic");
|
||||
}
|
||||
salt = Arrays.copyOfRange(decodedHeader, HEADER_MAGIC_1.length(), decodedHeader.length);
|
||||
} catch(IllegalArgumentException e) {
|
||||
throw new StorageException("Not a Sparrow wallet - invalid header");
|
||||
}
|
||||
} else {
|
||||
SecureRandom secureRandom = new SecureRandom();
|
||||
secureRandom.nextBytes(salt);
|
||||
}
|
||||
|
||||
return new Argon2KeyDeriver(salt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersistenceType getType() {
|
||||
return PersistenceType.JSON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted(File walletFile) throws IOException {
|
||||
FileType fileType = IOUtils.getFileType(walletFile);
|
||||
if(FileType.JSON.equals(fileType)) {
|
||||
return false;
|
||||
} else if(FileType.BINARY.equals(fileType)) {
|
||||
try(FileInputStream fileInputStream = new FileInputStream(walletFile)) {
|
||||
getWalletKeyDeriver(fileInputStream);
|
||||
return true;
|
||||
} catch(StorageException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("Unsupported file type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletId(Storage storage, Wallet wallet) {
|
||||
return storage.getWalletFile().getParentFile().getAbsolutePath() + File.separator + getWalletName(storage.getWalletFile(), null) + ":" + (wallet == null || wallet.isMasterWallet() ? "master" : wallet.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletName(File walletFile, Wallet wallet) {
|
||||
String name = walletFile.getName();
|
||||
if(name.endsWith("." + getType().getExtension())) {
|
||||
name = name.substring(0, name.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyWallet(File walletFile, OutputStream outputStream) throws IOException {
|
||||
com.google.common.io.Files.copy(walletFile, outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
//Nothing required
|
||||
}
|
||||
|
||||
public static Gson getGson() {
|
||||
return getGson(true);
|
||||
}
|
||||
|
||||
private static Gson getGson(boolean includeWalletSerializers) {
|
||||
GsonBuilder gsonBuilder = new GsonBuilder();
|
||||
gsonBuilder.registerTypeAdapter(ExtendedKey.class, new ExtendedPublicKeySerializer());
|
||||
gsonBuilder.registerTypeAdapter(ExtendedKey.class, new ExtendedPublicKeyDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(byte[].class, new ByteArraySerializer());
|
||||
gsonBuilder.registerTypeAdapter(byte[].class, new ByteArrayDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(Sha256Hash.class, new Sha256HashSerializer());
|
||||
gsonBuilder.registerTypeAdapter(Sha256Hash.class, new Sha256HashDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(Date.class, new DateSerializer());
|
||||
gsonBuilder.registerTypeAdapter(Date.class, new DateDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionSerializer());
|
||||
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionDeserializer());
|
||||
if(includeWalletSerializers) {
|
||||
gsonBuilder.registerTypeAdapter(Keystore.class, new KeystoreSerializer());
|
||||
gsonBuilder.registerTypeAdapter(WalletNode.class, new NodeSerializer());
|
||||
gsonBuilder.registerTypeAdapter(WalletNode.class, new NodeDeserializer());
|
||||
}
|
||||
|
||||
gsonBuilder.addSerializationExclusionStrategy(new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes field) {
|
||||
return field.getName().equals("id") || field.getName().equals("masterWallet") || field.getName().equals("childWallets");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> clazz) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return gsonBuilder.setPrettyPrinting().disableHtmlEscaping().create();
|
||||
}
|
||||
|
||||
private static class ExtendedPublicKeySerializer implements JsonSerializer<ExtendedKey> {
|
||||
@Override
|
||||
public JsonElement serialize(ExtendedKey src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class ExtendedPublicKeyDeserializer implements JsonDeserializer<ExtendedKey> {
|
||||
@Override
|
||||
public ExtendedKey deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return ExtendedKey.fromDescriptor(json.getAsJsonPrimitive().getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class ByteArraySerializer implements JsonSerializer<byte[]> {
|
||||
@Override
|
||||
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(Utils.bytesToHex(src));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ByteArrayDeserializer implements JsonDeserializer<byte[]> {
|
||||
@Override
|
||||
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return Utils.hexToBytes(json.getAsJsonPrimitive().getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class Sha256HashSerializer implements JsonSerializer<Sha256Hash> {
|
||||
@Override
|
||||
public JsonElement serialize(Sha256Hash src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class Sha256HashDeserializer implements JsonDeserializer<Sha256Hash> {
|
||||
@Override
|
||||
public Sha256Hash deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return Sha256Hash.wrap(json.getAsJsonPrimitive().getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DateSerializer implements JsonSerializer<Date> {
|
||||
@Override
|
||||
public JsonElement serialize(Date src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.getTime());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DateDeserializer implements JsonDeserializer<Date> {
|
||||
@Override
|
||||
public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return new Date(json.getAsJsonPrimitive().getAsLong());
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionSerializer implements JsonSerializer<Transaction> {
|
||||
@Override
|
||||
public JsonElement serialize(Transaction src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
src.bitcoinSerializeToStream(baos);
|
||||
return new JsonPrimitive(Utils.bytesToHex(baos.toByteArray()));
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionDeserializer implements JsonDeserializer<Transaction> {
|
||||
@Override
|
||||
public Transaction deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
byte[] rawTx = Utils.hexToBytes(json.getAsJsonPrimitive().getAsString());
|
||||
return new Transaction(rawTx);
|
||||
}
|
||||
}
|
||||
|
||||
private static class KeystoreSerializer implements JsonSerializer<Keystore> {
|
||||
@Override
|
||||
public JsonElement serialize(Keystore keystore, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(keystore);
|
||||
if(keystore.hasPrivateKey()) {
|
||||
jsonObject.remove("extendedPublicKey");
|
||||
jsonObject.getAsJsonObject("keyDerivation").remove("masterFingerprint");
|
||||
}
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
}
|
||||
|
||||
private static class NodeSerializer implements JsonSerializer<WalletNode> {
|
||||
@Override
|
||||
public JsonElement serialize(WalletNode node, Type typeOfSrc, JsonSerializationContext context) {
|
||||
JsonObject jsonObject = (JsonObject)getGson(false).toJsonTree(node);
|
||||
|
||||
JsonArray children = jsonObject.getAsJsonArray("children");
|
||||
Iterator<JsonElement> iter = children.iterator();
|
||||
while(iter.hasNext()) {
|
||||
JsonObject childObject = (JsonObject)iter.next();
|
||||
removeEmptyCollection(childObject, "children");
|
||||
removeEmptyCollection(childObject, "transactionOutputs");
|
||||
|
||||
if(childObject.get("label") == null && childObject.get("children") == null && childObject.get("transactionOutputs") == null) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
private void removeEmptyCollection(JsonObject jsonObject, String memberName) {
|
||||
if(jsonObject.get(memberName) != null && jsonObject.getAsJsonArray(memberName).size() == 0) {
|
||||
jsonObject.remove(memberName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class NodeDeserializer implements JsonDeserializer<WalletNode> {
|
||||
@Override
|
||||
public WalletNode deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
WalletNode node = getGson(false).fromJson(json, typeOfT);
|
||||
node.parseDerivation();
|
||||
|
||||
for(WalletNode childNode : node.getChildren()) {
|
||||
childNode.parseDerivation();
|
||||
if(childNode.getChildren() == null) {
|
||||
childNode.setChildren(new TreeSet<>());
|
||||
}
|
||||
if(childNode.getTransactionOutputs() == null) {
|
||||
childNode.setTransactionOutputs(new TreeSet<>());
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public class KeystoneMultisig extends ColdcardMultisig {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Keystone Multisig";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.KEYSTONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = super.getKeystore(scriptType, inputStream, password);
|
||||
keystore.setLabel("Keystone");
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import file or QR created by using the Multisig Wallet > ... > Show/Export XPUB feature on your Keystone.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
Wallet wallet = super.importWallet(inputStream, password);
|
||||
for(Keystore keystore : wallet.getKeystores()) {
|
||||
keystore.setLabel(keystore.getLabel().replace("Coldcard", "Keystone"));
|
||||
keystore.setWalletModel(WalletModel.KEYSTONE);
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return "Import file or QR created by using the Multisig Wallet > ... > Create Multisig Wallet feature on your Keystone.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "Export file or QR that can be read by your Keystone using the Multisig Wallet > ... > Import Multisig Wallet feature.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportScannable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
|
||||
private static final Logger log = LoggerFactory.getLogger(KeystoneSinglesig.class);
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Keystone";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import file or QR created by using the My Keystone > ... > Export Wallet feature on your Keystone. Make sure to set the Watch-only Wallet to Sparrow in the Settings first.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.KEYSTONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
String outputDescriptor = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n"));
|
||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(outputDescriptor);
|
||||
|
||||
if(descriptor.isMultisig()) {
|
||||
throw new IllegalArgumentException("Output descriptor describes a multisig wallet");
|
||||
}
|
||||
|
||||
if(descriptor.getScriptType() != scriptType) {
|
||||
throw new IllegalArgumentException("Output descriptor describes a " + descriptor.getScriptType().getDescription() + " wallet");
|
||||
}
|
||||
|
||||
ExtendedKey xpub = descriptor.getSingletonExtendedPublicKey();
|
||||
KeyDerivation keyDerivation = descriptor.getKeyDerivation(xpub);
|
||||
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.KEYSTONE);
|
||||
keystore.setKeyDerivation(keyDerivation);
|
||||
keystore.setExtendedPublicKey(xpub);
|
||||
|
||||
return keystore;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Error getting " + getName() + " keystore - not an output descriptor");
|
||||
throw new ImportException("Error getting " + getName() + " keystore", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting " + getName() + " keystore", e);
|
||||
throw new ImportException("Error getting " + getName() + " keystore", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return getKeystoreImportDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
//Use default of P2WPKH
|
||||
Keystore keystore = getKeystore(ScriptType.P2WPKH, inputStream, "");
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(ScriptType.P2WPKH);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2WPKH, wallet.getKeystores(), null));
|
||||
|
||||
try {
|
||||
wallet.checkWallet();
|
||||
} catch(InvalidWalletException e) {
|
||||
throw new ImportException("Imported " + getName() + " wallet was invalid: " + e.getMessage());
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted(File file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public interface Persistence {
|
||||
WalletBackupAndKey loadWallet(Storage storage) throws IOException, StorageException;
|
||||
WalletBackupAndKey loadWallet(Storage storage, CharSequence password) throws IOException, StorageException;
|
||||
WalletBackupAndKey loadWallet(Storage storage, CharSequence password, ECKey alreadyDerivedKey) throws IOException, StorageException;
|
||||
File storeWallet(Storage storage, Wallet wallet) throws IOException, StorageException;
|
||||
File storeWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException, StorageException;
|
||||
void updateWallet(Storage storage, Wallet wallet) throws IOException, StorageException;
|
||||
void updateWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws IOException, StorageException;
|
||||
ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException;
|
||||
AsymmetricKeyDeriver getKeyDeriver();
|
||||
void setKeyDeriver(AsymmetricKeyDeriver keyDeriver);
|
||||
PersistenceType getType();
|
||||
boolean isEncrypted(File walletFile) throws IOException;
|
||||
String getWalletId(Storage storage, Wallet wallet);
|
||||
String getWalletName(File walletFile, Wallet wallet);
|
||||
void copyWallet(File walletFile, OutputStream outputStream) throws IOException;
|
||||
void close();
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.sparrow.io.db.DbPersistence;
|
||||
|
||||
public enum PersistenceType {
|
||||
JSON("json") {
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Persistence getInstance() {
|
||||
return new JsonPersistence();
|
||||
}
|
||||
},
|
||||
DB("db") {
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return "mv.db";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Persistence getInstance() {
|
||||
return new DbPersistence();
|
||||
}
|
||||
};
|
||||
|
||||
private final String name;
|
||||
|
||||
private PersistenceType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public abstract String getExtension();
|
||||
|
||||
public abstract Persistence getInstance();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
public class SeedSigner extends SpecterDIY {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "SeedSigner";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import QR created on your SeedSigner by selecting Generate XPUB in the Signing Tools menu. Note that SeedSigner currently only supports P2WSH Multisig wallets.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.SEEDSIGNER;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user