mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebd629db3a | ||
|
|
c18a2f4388 | ||
|
|
6f95dbe309 | ||
|
|
576253e651 | ||
|
|
b9d6cb17d4 | ||
|
|
3b730a1711 | ||
|
|
8c0a1932cf | ||
|
|
35b57f9d69 | ||
|
|
d6ad7f4808 | ||
|
|
3e47a49f49 | ||
|
|
4c817d243d | ||
|
|
22b7b659f3 | ||
|
|
9dd6068e69 | ||
|
|
54baee57e1 | ||
|
|
581aaf288e | ||
|
|
2fa47e640d | ||
|
|
ea03dece72 | ||
|
|
8e0b9a3ea0 | ||
|
|
67179127e3 | ||
|
|
4ebee8a8f3 | ||
|
|
2548e77d90 | ||
|
|
58e3b9dcdd | ||
|
|
429b733140 | ||
|
|
56e3a54ae0 | ||
|
|
a934ffa76c | ||
|
|
86a49e0d9a | ||
|
|
761e9c9b7e | ||
|
|
712241873f | ||
|
|
31f9cca33a | ||
|
|
395e90e2a5 | ||
|
|
b2657cdcfb | ||
|
|
6bbae204a6 | ||
|
|
0b55dd8a1e | ||
|
|
f74287697c | ||
|
|
7a3e1dfa1f | ||
|
|
427a6925ee | ||
|
|
c55b19af0f | ||
|
|
e12f7a634a | ||
|
|
ca1f934138 | ||
|
|
4f9b87b74e | ||
|
|
e1e5df78c6 | ||
|
|
cfd06a8513 | ||
|
|
1c1099217b | ||
|
|
6939d8a06a | ||
|
|
dbebade3ab | ||
|
|
5895837b60 | ||
|
|
0f9e9d9c35 | ||
|
|
59452e64ea | ||
|
|
573d5b2c18 | ||
|
|
2f0dc5bea8 | ||
|
|
27f7d5ad38 | ||
|
|
414ffc2648 | ||
|
|
aaca9ffa16 | ||
|
|
44b9cee825 | ||
|
|
c024c351ac | ||
|
|
ed2914f2aa | ||
|
|
0b4785e01c | ||
|
|
0c50c9cb9c | ||
|
|
916c2b6122 | ||
|
|
308a89c958 | ||
|
|
88ebef97d4 | ||
|
|
a42761981c | ||
|
|
b6f047d382 | ||
|
|
8f63d89be8 | ||
|
|
6e6111b47c | ||
|
|
e8af7c70bd | ||
|
|
2fc551e35b | ||
|
|
adb77771aa | ||
|
|
13e01451b7 | ||
|
|
aa10bcfe1a | ||
|
|
f30c00ba8f | ||
|
|
4577a64ad5 | ||
|
|
7371ca2994 | ||
|
|
050c4fc31e | ||
|
|
772370808c | ||
|
|
b22e891b7d | ||
|
|
57b3214c54 | ||
|
|
615b78b497 | ||
|
|
d1ab1db1c5 | ||
|
|
f5ac6a3b73 | ||
|
|
fec45356a2 | ||
|
|
37c4ff4dd7 | ||
|
|
b0877d94bf | ||
|
|
a3e4342d7d | ||
|
|
90355a653f | ||
|
|
6339c3a7d7 | ||
|
|
8f7f0d4c61 | ||
|
|
74b4f51640 | ||
|
|
2caee79df4 | ||
|
|
34b4c39ccd | ||
|
|
34d1571669 | ||
|
|
22465b1b65 | ||
|
|
b91e8eab51 | ||
|
|
55e69bf263 | ||
|
|
02d3817cb1 | ||
|
|
fc9cdaabb4 | ||
|
|
2aa3d83402 | ||
|
|
fc5d6ada36 | ||
|
|
be599fb003 | ||
|
|
d79c4a13f6 | ||
|
|
e6fce14fde | ||
|
|
5c3a00b71b | ||
|
|
ced4d4d337 | ||
|
|
4a3ad9f4ff | ||
|
|
f5a72105ac | ||
|
|
0502eec0cd | ||
|
|
8e6933b5ca | ||
|
|
1fd1dec6cf | ||
|
|
422713ff53 | ||
|
|
ada45ee75b | ||
|
|
2f153686dd | ||
|
|
f691f1691e | ||
|
|
1f9e37b40c | ||
|
|
a1d2de1859 | ||
|
|
143472bdfc | ||
|
|
b9e64d42ff | ||
|
|
9a09bb8cda | ||
|
|
4b028af123 | ||
|
|
8033e5fd88 | ||
|
|
badf8c8f2f | ||
|
|
b6a353815c | ||
|
|
ea2f858dc9 | ||
|
|
324540009a | ||
|
|
1c1f90344f | ||
|
|
094dd45547 | ||
|
|
6d434722cc | ||
|
|
c8a4ed0c3d |
@@ -12,16 +12,11 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 15.0.2
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: joschi/setup-jdk@v2
|
||||
- name: Set up JDK 16.0.1
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
java-version: 15
|
||||
- name: Set up JDK 14.0.2
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 14.0.2
|
||||
distribution: 'adopt'
|
||||
java-version: '16.0.1'
|
||||
- name: Show Build Versions
|
||||
run: ./gradlew -v
|
||||
- name: Cache Gradle packages
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[submodule "drongo"]
|
||||
path = drongo
|
||||
url = git@github.com:sparrowwallet/drongo.git
|
||||
url = ../../sparrowwallet/drongo.git
|
||||
|
||||
@@ -8,11 +8,18 @@ More information (and release binaries) can be found at https://sparrowwallet.co
|
||||
|
||||
## Building
|
||||
|
||||
To clone this project, use
|
||||
To clone this project, use
|
||||
|
||||
`git clone --recursive git@github.com:sparrowwallet/sparrow.git`
|
||||
|
||||
In order to build, Sparrow requires Java 14 to be installed. The release packages can be built using
|
||||
or for those without SSH credentials:
|
||||
|
||||
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
|
||||
|
||||
In order to build, Sparrow requires Java 16 to be installed.
|
||||
The release binaries are built with [AdoptOpenJdk 16.0.1+9 Hotspot](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
|
||||
|
||||
These binaries can be built from source using
|
||||
|
||||
`./gradlew jpackage`
|
||||
|
||||
@@ -20,7 +27,7 @@ When updating to the latest HEAD
|
||||
|
||||
`git pull --recurse-submodules`
|
||||
|
||||
All jar files created are reproducible builds.
|
||||
The release binaries are reproducible from v1.5.0 onwards (pre codesigning and installer packaging). More detailed [instructions on reproducing the binaries](docs/reproducible.md) are provided.
|
||||
|
||||
## Running
|
||||
|
||||
@@ -28,7 +35,7 @@ If you prefer to run Sparrow directly from source, it can be launched with
|
||||
|
||||
`./sparrow`
|
||||
|
||||
Java 14 or higher must be installed.
|
||||
Java 16 or higher must be installed.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
+369
-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.2'
|
||||
def sparrowVersion = '1.5.1'
|
||||
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' ]
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ java {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
//Any changes to the dependencies must be reflected in the module definitions below!
|
||||
implementation(project(':drongo')) {
|
||||
exclude group: 'org.hamcrest'
|
||||
exclude group: 'junit'
|
||||
@@ -53,19 +54,28 @@ dependencies {
|
||||
implementation('org.flywaydb:flyway-core:7.10.5-SNAPSHOT')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||
implementation('com.google.zxing:javase:3.4.0')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.0')
|
||||
implementation('com.google.zxing:javase:3.4.0') {
|
||||
exclude group: 'com.beust', module: 'jcommander'
|
||||
}
|
||||
implementation('com.beust:jcommander:1.81')
|
||||
implementation('com.github.arteam:simple-json-rpc-core:1.0')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.0') {
|
||||
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
||||
}
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.0')
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.2')
|
||||
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
}
|
||||
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
|
||||
exclude group: 'com.nativelibs4java', module: 'bridj'
|
||||
}
|
||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8")
|
||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8") {
|
||||
exclude group: 'org.jetbrains.kotlin'
|
||||
}
|
||||
implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20')
|
||||
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
@@ -76,15 +86,19 @@ 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'
|
||||
}
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.18-SNAPSHOT')
|
||||
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 {
|
||||
@@ -103,6 +117,10 @@ processResources {
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
|
||||
}
|
||||
|
||||
run {
|
||||
applicationDefaultJvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||
@@ -120,7 +138,8 @@ run {
|
||||
"--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-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson"]
|
||||
|
||||
if(os.macOsX) {
|
||||
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
||||
@@ -135,14 +154,17 @@ 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'
|
||||
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
|
||||
uses 'org.eclipse.jetty.websocket.api.extensions.Extension'
|
||||
uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory'
|
||||
}
|
||||
|
||||
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png']
|
||||
options = ['--strip-debug', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
launcher {
|
||||
name = 'sparrow'
|
||||
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
@@ -154,17 +176,22 @@ jlink {
|
||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=com.sparrowwallet.merged.module",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.sql",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow"]
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
|
||||
"--add-reads=com.sparrowwallet.merged.module=logback.classic",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core"]
|
||||
|
||||
if(os.macOsX) {
|
||||
jvmArgs += "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"
|
||||
@@ -175,7 +202,7 @@ jlink {
|
||||
imageName = "Sparrow"
|
||||
installerName = "Sparrow"
|
||||
appVersion = "${sparrowVersion}"
|
||||
skipInstaller = os.macOsX
|
||||
skipInstaller = os.macOsX || properties.skipInstallers
|
||||
imageOptions = []
|
||||
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) {
|
||||
@@ -216,3 +243,324 @@ task packageTarDistribution(type: Tar) {
|
||||
include "Sparrow/**"
|
||||
}
|
||||
}
|
||||
|
||||
extraJavaModuleInfo {
|
||||
module('jackson-core-2.10.1.jar', 'com.fasterxml.jackson.core', '2.10.1') {
|
||||
exports('com.fasterxml.jackson.core')
|
||||
exports('com.fasterxml.jackson.core.async')
|
||||
exports('com.fasterxml.jackson.core.base')
|
||||
exports('com.fasterxml.jackson.core.exc')
|
||||
exports('com.fasterxml.jackson.core.filter')
|
||||
exports('com.fasterxml.jackson.core.format')
|
||||
exports('com.fasterxml.jackson.core.io')
|
||||
exports('com.fasterxml.jackson.core.json')
|
||||
exports('com.fasterxml.jackson.core.json.async')
|
||||
exports('com.fasterxml.jackson.core.sym')
|
||||
exports('com.fasterxml.jackson.core.type')
|
||||
exports('com.fasterxml.jackson.core.util')
|
||||
uses('com.fasterxml.jackson.core.ObjectCodec')
|
||||
}
|
||||
module('jackson-annotations-2.10.1.jar', 'com.fasterxml.jackson.annotation', '2.10.1') {
|
||||
requires('com.fasterxml.jackson.core')
|
||||
exports('com.fasterxml.jackson.annotation')
|
||||
}
|
||||
module('jackson-databind-2.10.1.jar', 'com.fasterxml.jackson.databind', '2.10.1') {
|
||||
requires('java.desktop')
|
||||
requires('java.logging')
|
||||
requires('com.fasterxml.jackson.annotation')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
requires('java.sql')
|
||||
requires('java.xml')
|
||||
exports('com.fasterxml.jackson.databind')
|
||||
exports('com.fasterxml.jackson.databind.annotation')
|
||||
exports('com.fasterxml.jackson.databind.cfg')
|
||||
exports('com.fasterxml.jackson.databind.deser')
|
||||
exports('com.fasterxml.jackson.databind.deser.impl')
|
||||
exports('com.fasterxml.jackson.databind.deser.std')
|
||||
exports('com.fasterxml.jackson.databind.exc')
|
||||
exports('com.fasterxml.jackson.databind.ext')
|
||||
exports('com.fasterxml.jackson.databind.introspect')
|
||||
exports('com.fasterxml.jackson.databind.json')
|
||||
exports('com.fasterxml.jackson.databind.jsonFormatVisitors')
|
||||
exports('com.fasterxml.jackson.databind.jsonschema')
|
||||
exports('com.fasterxml.jackson.databind.jsontype')
|
||||
exports('com.fasterxml.jackson.databind.jsontype.impl')
|
||||
exports('com.fasterxml.jackson.databind.module')
|
||||
exports('com.fasterxml.jackson.databind.node')
|
||||
exports('com.fasterxml.jackson.databind.ser')
|
||||
exports('com.fasterxml.jackson.databind.ser.impl')
|
||||
exports('com.fasterxml.jackson.databind.ser.std')
|
||||
exports('com.fasterxml.jackson.databind.type')
|
||||
exports('com.fasterxml.jackson.databind.util')
|
||||
uses('com.fasterxml.jackson.databind.Module')
|
||||
}
|
||||
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.core')
|
||||
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')
|
||||
exports('com.google.common.primitives')
|
||||
exports('com.google.common.math')
|
||||
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.2.jar', 'com.sparrowwallet.hummingbird', '1.6.2') {
|
||||
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('nightjar-0.2.18-SNAPSHOT.jar', 'com.sparrowwallet.nightjar', '0.2.18-SNAPSHOT') {
|
||||
requires('com.google.common')
|
||||
requires('net.sourceforge.streamsupport')
|
||||
requires('org.slf4j')
|
||||
requires('org.bouncycastle.provider')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('com.fasterxml.jackson.annotation')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
requires('logback.classic')
|
||||
requires('org.json')
|
||||
exports('com.samourai.http.client')
|
||||
exports('com.samourai.tor.client')
|
||||
exports('com.samourai.wallet.api.backend')
|
||||
exports('com.samourai.wallet.api.backend.beans')
|
||||
exports('com.samourai.wallet.client.indexHandler')
|
||||
exports('com.samourai.wallet.hd')
|
||||
exports('com.samourai.wallet.util')
|
||||
exports('com.samourai.whirlpool.client.event')
|
||||
exports('com.samourai.whirlpool.client.wallet')
|
||||
exports('com.samourai.whirlpool.client.wallet.beans')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.dataSource')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.dataPersister')
|
||||
exports('com.samourai.whirlpool.client.whirlpool')
|
||||
exports('com.samourai.whirlpool.client.whirlpool.beans')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.pool')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.utxo')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.utxoConfig')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.supplier')
|
||||
exports('com.samourai.whirlpool.client.mix.handler')
|
||||
exports('com.samourai.whirlpool.client.mix.listener')
|
||||
exports('com.samourai.whirlpool.protocol.beans')
|
||||
exports('com.samourai.whirlpool.protocol.rest')
|
||||
exports('com.samourai.whirlpool.client.tx0')
|
||||
exports('com.samourai.wallet.segwit.bech32')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.wallet')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.walletState')
|
||||
exports('com.sparrowwallet.nightjar.http')
|
||||
exports('com.sparrowwallet.nightjar.stomp')
|
||||
exports('com.sparrowwallet.nightjar.tor')
|
||||
}
|
||||
module('throwing-supplier-1.0.3.jar', 'zeroleak.throwingsupplier', '1.0.3') {
|
||||
exports('com.zeroleak.throwingsupplier')
|
||||
}
|
||||
module('okhttp-2.7.5.jar', 'com.squareup.okhttp', '2.7.5') {
|
||||
exports('com.squareup.okhttp')
|
||||
}
|
||||
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
|
||||
exports('okio')
|
||||
}
|
||||
module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') {
|
||||
exports('com.auth0.jwt')
|
||||
}
|
||||
module('json-20180130.jar', 'org.json', '1.0') {
|
||||
exports('org.json')
|
||||
}
|
||||
module('scrypt-1.4.0.jar', 'com.lambdaworks.scrypt', '1.4.0') {
|
||||
exports('com.lambdaworks.codec')
|
||||
exports('com.lambdaworks.crypto')
|
||||
}
|
||||
module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
|
||||
requires('jdk.unsupported')
|
||||
exports('java8.util')
|
||||
exports('java8.util.function')
|
||||
exports('java8.util.stream')
|
||||
}
|
||||
module('protobuf-java-2.6.1.jar', 'com.google.protobuf', '2.6.1') {
|
||||
exports('com.google.protobuf')
|
||||
}
|
||||
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
|
||||
exports('org.apache.commons.text')
|
||||
}
|
||||
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
|
||||
exports('net.jcip.annotations')
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
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();
|
||||
|
||||
//Recreate jackson jars as open, non-synthetic modules
|
||||
if (isModule(originalJar) && !originalJarName.contains("jackson")) {
|
||||
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))) {
|
||||
Manifest manifest = inputStream.getManifest();
|
||||
if(manifest == null) {
|
||||
manifest = new Manifest();
|
||||
}
|
||||
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), manifest)) {
|
||||
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) {
|
||||
if(!jarEntry.getName().equals("module-info.class")) {
|
||||
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);
|
||||
}
|
||||
for (String usesName : moduleInfo.getUses()) {
|
||||
moduleVisitor.visitUse(usesName.replace('.', '/'));
|
||||
}
|
||||
moduleVisitor.visitEnd();
|
||||
classWriter.visitEnd();
|
||||
return classWriter.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
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<>();
|
||||
private List<String> uses = 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 void uses(String uses) {
|
||||
this.uses.add(uses);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
protected List<String> getUses() {
|
||||
return uses;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
## Sparrow Wallet Repository Docs
|
||||
|
||||
Note that most documentation for the project can be found at https://sparrowwallet.com/docs/.
|
||||
The documentation here is mainly developer-related resources.
|
||||
|
||||
### [Reproducible builds](reproducible.md)
|
||||
|
||||
Documentation to create and verify a build of the project against the released binaries.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
## Reproducible builds
|
||||
|
||||
Reproducibility is a goal of the Sparrow Wallet project.
|
||||
As of v1.5.0 and later, it is possible to recreate the exact binaries in the Github releases (specifically, the contents of the `.tar.gz` and `.zip` files).
|
||||
|
||||
Due to minor variances, it is not yet possible to reproduce the installer packages (`.deb`, `.rpm` and `.exe`).
|
||||
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
|
||||
Work on resolving both of these issues is ongoing.
|
||||
|
||||
### Reproducing a release
|
||||
|
||||
#### Install Java
|
||||
|
||||
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
|
||||
For v1.5.0 and later, this is AdoptOpenJdk jdk-16.0.1+9 Hotspot.
|
||||
It is available for all supported platforms from the [AdoptOpenJdk site](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
|
||||
|
||||
For reference, the downloads are as follows:
|
||||
- [Linux x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz)
|
||||
- [MacOS x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_mac_hotspot_16.0.1_9.tar.gz)
|
||||
- [Windows x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_windows_hotspot_16.0.1_9.zip)
|
||||
|
||||
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
|
||||
```shell
|
||||
sudo apt-get install -y wget apt-transport-https gnupg
|
||||
wget https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public
|
||||
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --import public
|
||||
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --export --output adoptopenjdk-archive-keyring.gpg
|
||||
rm adoptopenjdk-keyring.gpg
|
||||
sudo mv adoptopenjdk-archive-keyring.gpg /usr/share/keyrings
|
||||
echo "deb [signed-by=/usr/share/keyrings/adoptopenjdk-archive-keyring.gpg] https://adoptopenjdk.jfrog.io/adoptopenjdk/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/adoptopenjdk.list
|
||||
sudo apt update -y
|
||||
sudo apt-get install -y adoptopenjdk-16-hotspot=16.0.1+9-3
|
||||
```
|
||||
|
||||
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
|
||||
See the installation [instructions here](https://sdkman.io/install).
|
||||
Once installed, run
|
||||
```shell
|
||||
sdk install java 16.0.1.hs-adpt
|
||||
```
|
||||
|
||||
#### Other requirements
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
```shell
|
||||
sudo apt install -y rpm fakeroot binutils
|
||||
```
|
||||
|
||||
#### Building the binaries
|
||||
|
||||
The project can cloned for a specific release tag as follows:
|
||||
```shell
|
||||
GIT_TAG="1.5.1"
|
||||
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
|
||||
```
|
||||
|
||||
Thereafter, building should be straightforward:
|
||||
|
||||
```shell
|
||||
cd sparrow
|
||||
./gradlew jpackage
|
||||
```
|
||||
|
||||
The binaries (and installers) will be placed in the `build/jpackage` folder.
|
||||
|
||||
#### Verifying the binaries are identical
|
||||
|
||||
Note that you will be verifying the files in the `build/jpackage/Sparrow` folder against either the `.tar.gz` or `.zip` releases.
|
||||
Download either of these depending on your platform and extract the contents to a folder (in the following example, `/tmp`).
|
||||
Then compare all of the folders and files recursively:
|
||||
|
||||
```shell
|
||||
diff -r build/jpackage/Sparrow /tmp/Sparrow
|
||||
```
|
||||
|
||||
This command should have no output indicating that the two folders (and all their contents) are identical.
|
||||
|
||||
+1
-1
Submodule drongo updated: c021300797...025af05758
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
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
cd `dirname $0`
|
||||
args="$*"
|
||||
args="${args%"${args##*[![:space:]]}"}"
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.2</string>
|
||||
<string>1.5.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -3,10 +3,7 @@ package com.sparrowwallet.sparrow;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.io.ByteSource;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
@@ -33,16 +30,22 @@ import com.sparrowwallet.sparrow.transaction.TransactionData;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import de.codecentric.centerdevice.MenuToolkit;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
@@ -117,31 +120,44 @@ public class AppController implements Initializable {
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem openWalletsInNewWindows;
|
||||
private static final BooleanProperty openWalletsInNewWindowsProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem hideEmptyUsedAddresses;
|
||||
private static final BooleanProperty hideEmptyUsedAddressesProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem useHdCameraResolution;
|
||||
private static final BooleanProperty useHdCameraResolutionProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem showLoadingLog;
|
||||
private static final BooleanProperty showLoadingLogProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem showUtxosChart;
|
||||
private static final BooleanProperty showUtxosChartProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem showTxHex;
|
||||
private static final BooleanProperty showTxHexProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private MenuItem minimizeToTray;
|
||||
|
||||
@FXML
|
||||
private MenuItem lockWallet;
|
||||
|
||||
@FXML
|
||||
private MenuItem refreshWallet;
|
||||
|
||||
@FXML
|
||||
private MenuItem sendToMany;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem preventSleep;
|
||||
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private StackPane rootStack;
|
||||
|
||||
@@ -160,6 +176,12 @@ public class AppController implements Initializable {
|
||||
|
||||
private final Set<Wallet> loadingWallets = new LinkedHashSet<>();
|
||||
|
||||
private final Set<Wallet> emptyLoadingWallets = new LinkedHashSet<>();
|
||||
|
||||
private final ChangeListener<Boolean> serverToggleOnlineListener = (observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
|
||||
};
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
@@ -221,8 +243,8 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
|
||||
}
|
||||
|
||||
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().map(tab -> (TabData)tab.getUserData())
|
||||
.filter(tabData -> tabData.getType() == TabData.TabType.WALLET).map(tabData -> (WalletTabData)tabData).collect(Collectors.toList());
|
||||
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().filter(tab -> tab.getUserData() instanceof WalletTabData)
|
||||
.flatMap(tab -> ((TabPane)tab.getContent()).getTabs().stream().map(subTab -> (WalletTabData)subTab.getUserData())).collect(Collectors.toList());
|
||||
if(!closedWalletTabs.isEmpty()) {
|
||||
EventManager.get().post(new WalletTabsClosedEvent(closedWalletTabs));
|
||||
}
|
||||
@@ -266,16 +288,26 @@ public class AppController implements Initializable {
|
||||
selectedThemeToggle.ifPresent(toggle -> theme.selectToggle(toggle));
|
||||
setTheme(null);
|
||||
|
||||
openWalletsInNewWindows.setSelected(Config.get().isOpenWalletsInNewWindows());
|
||||
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());
|
||||
openWalletsInNewWindowsProperty.set(Config.get().isOpenWalletsInNewWindows());
|
||||
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
|
||||
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
|
||||
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
|
||||
useHdCameraResolutionProperty.set(Config.get().isHdCapture());
|
||||
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
|
||||
showTxHexProperty.set(Config.get().isShowTransactionHex());
|
||||
showTxHex.selectedProperty().bindBidirectional(showTxHexProperty);
|
||||
showLoadingLogProperty.set(Config.get().isShowLoadingLog());
|
||||
showLoadingLog.selectedProperty().bindBidirectional(showLoadingLogProperty);
|
||||
showUtxosChartProperty.set(Config.get().isShowUtxosChart());
|
||||
showUtxosChart.selectedProperty().bindBidirectional(showUtxosChartProperty);
|
||||
preventSleepProperty.set(Config.get().isPreventSleep());
|
||||
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
|
||||
|
||||
saveTransaction.setDisable(true);
|
||||
savePSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
|
||||
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
|
||||
exportWallet.setDisable(true);
|
||||
lockWallet.setDisable(true);
|
||||
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
|
||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||
|
||||
@@ -283,9 +315,7 @@ public class AppController implements Initializable {
|
||||
serverToggle.setSelected(isConnected());
|
||||
serverToggle.setDisable(Config.get().getServerType() == null);
|
||||
onlineProperty().bindBidirectional(serverToggle.selectedProperty());
|
||||
onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
|
||||
});
|
||||
onlineProperty().addListener(new WeakChangeListener<>(serverToggleOnlineListener));
|
||||
serverToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
Config.get().setMode(serverToggle.isSelected() ? Mode.ONLINE : Mode.OFFLINE);
|
||||
});
|
||||
@@ -618,13 +648,36 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void copyPSBTHex(ActionEvent event) {
|
||||
copyPSBT(false);
|
||||
}
|
||||
|
||||
public void copyPSBTBase64(ActionEvent event) {
|
||||
copyPSBT(true);
|
||||
}
|
||||
|
||||
public void copyPSBT(boolean asBase64) {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
String data = asBase64 ? transactionTabData.getPsbt().toBase64String() : transactionTabData.getPsbt().toString();
|
||||
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(data);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
}
|
||||
}
|
||||
|
||||
public List<WalletTabData> getOpenWalletTabData() {
|
||||
List<WalletTabData> openWalletTabData = new ArrayList<>();
|
||||
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.WALLET) {
|
||||
openWalletTabData.add((WalletTabData)tabData);
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)tab.getContent();
|
||||
for(Tab subTab : subTabs.getTabs()) {
|
||||
openWalletTabData.add((WalletTabData)subTab.getUserData());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,11 +696,14 @@ public class AppController implements Initializable {
|
||||
|
||||
public void selectTab(Wallet wallet) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData) tab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.WALLET) {
|
||||
WalletTabData walletTabData = (WalletTabData) tabData;
|
||||
if(walletTabData.getWallet() == wallet) {
|
||||
tabs.getSelectionModel().select(tab);
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)tab.getContent();
|
||||
for(Tab subTab : subTabs.getTabs()) {
|
||||
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
|
||||
if(walletTabData.getWallet() == wallet) {
|
||||
tabs.getSelectionModel().select(tab);
|
||||
subTabs.getSelectionModel().select(subTab);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -707,6 +763,12 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new BitcoinUnitChangedEvent(unit));
|
||||
}
|
||||
|
||||
public void preventSleep(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setPreventSleep(item.isSelected());
|
||||
AppServices.get().setPreventSleep(item.isSelected());
|
||||
}
|
||||
|
||||
public void openFile(File file) {
|
||||
if(isWalletFile(file)) {
|
||||
openWalletFile(file, true);
|
||||
@@ -801,7 +863,10 @@ public class AppController implements Initializable {
|
||||
} catch(StorageException e) {
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||
} catch(Exception e) {
|
||||
if(!attemptImportWallet(file, null)) {
|
||||
if(e instanceof IOException && e.getMessage().startsWith("The process cannot access the file because another process has locked")) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", "The wallet file is locked. Is another instance of " + MainApp.APP_NAME + " already running?");
|
||||
} else if(!attemptImportWallet(file, null)) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage() == null ? "Unsupported file format" : e.getMessage());
|
||||
}
|
||||
@@ -811,13 +876,13 @@ public class AppController implements Initializable {
|
||||
private void openWallet(Storage storage, WalletBackupAndKey walletBackupAndKey, AppController appController, boolean forceSameWindow) {
|
||||
try {
|
||||
checkWalletNetwork(walletBackupAndKey.getWallet());
|
||||
restorePublicKeysFromSeed(walletBackupAndKey.getWallet(), walletBackupAndKey.getKey());
|
||||
restorePublicKeysFromSeed(storage, 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);
|
||||
for(Map.Entry<WalletBackupAndKey, Storage> entry : walletBackupAndKey.getChildWallets().entrySet()) {
|
||||
openWallet(entry.getValue(), entry.getKey(), walletAppController, true);
|
||||
}
|
||||
Platform.runLater(() -> selectTab(walletBackupAndKey.getWallet()));
|
||||
} catch(Exception e) {
|
||||
@@ -834,19 +899,24 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
private void restorePublicKeysFromSeed(Wallet wallet, Key key) throws MnemonicException {
|
||||
private void restorePublicKeysFromSeed(Storage storage, Wallet wallet, Key key) throws MnemonicException {
|
||||
if(wallet.containsPrivateKeys()) {
|
||||
//Derive xpub and master fingerprint from seed, potentially with passphrase
|
||||
Wallet copy = wallet.copy();
|
||||
for(Keystore copyKeystore : copy.getKeystores()) {
|
||||
for(int i = 0; i < copy.getKeystores().size(); i++) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
if(copyKeystore.hasSeed()) {
|
||||
if(copyKeystore.getSeed().needsPassphrase()) {
|
||||
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getName(), copyKeystore);
|
||||
Optional<String> optionalPassphrase = passphraseDialog.showAndWait();
|
||||
if(optionalPassphrase.isPresent()) {
|
||||
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
|
||||
if(!wallet.isMasterWallet() && wallet.getMasterWallet().getKeystores().size() == copy.getKeystores().size() && wallet.getMasterWallet().getKeystores().get(i).hasSeed()) {
|
||||
copyKeystore.getSeed().setPassphrase(wallet.getMasterWallet().getKeystores().get(i).getSeed().getPassphrase());
|
||||
} else {
|
||||
return;
|
||||
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getFullName(), copyKeystore);
|
||||
Optional<String> optionalPassphrase = passphraseDialog.showAndWait();
|
||||
if(optionalPassphrase.isPresent()) {
|
||||
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
copyKeystore.getSeed().setPassphrase("");
|
||||
@@ -862,6 +932,18 @@ public class AppController implements Initializable {
|
||||
copy.decrypt(key);
|
||||
}
|
||||
|
||||
if(wallet.isWhirlpoolMasterWallet()) {
|
||||
String walletId = storage.getWalletId(wallet);
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
|
||||
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
|
||||
}
|
||||
|
||||
StandardAccount standardAccount = wallet.getStandardAccountType();
|
||||
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
|
||||
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
|
||||
}
|
||||
|
||||
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
||||
Keystore keystore = wallet.getKeystores().get(i);
|
||||
if(keystore.hasSeed()) {
|
||||
@@ -897,7 +979,8 @@ public class AppController implements Initializable {
|
||||
new SpecterDesktop(),
|
||||
new CoboVaultSinglesig(), new CoboVaultMultisig(),
|
||||
new PassportSinglesig(),
|
||||
new KeystoneSinglesig(), new KeystoneMultisig());
|
||||
new KeystoneSinglesig(), new KeystoneMultisig(),
|
||||
new CaravanMultisig());
|
||||
for(WalletImport importer : walletImporters) {
|
||||
try(FileInputStream inputStream = new FileInputStream(file)) {
|
||||
if(importer.isEncrypted(file) && password == null) {
|
||||
@@ -949,11 +1032,13 @@ public class AppController implements Initializable {
|
||||
//Close existing wallet first if open
|
||||
for(Iterator<Tab> iter = tabs.getTabs().iterator(); iter.hasNext(); ) {
|
||||
Tab tab = iter.next();
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.WALLET) {
|
||||
WalletTabData walletTabData = (WalletTabData) tabData;
|
||||
if(walletTabData.getStorage().getWalletFile().equals(walletFile)) {
|
||||
iter.remove();
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)tab.getContent();
|
||||
for(Tab subTab : subTabs.getTabs()) {
|
||||
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
|
||||
if(walletTabData.getStorage().getWalletFile().equals(walletFile)) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -974,8 +1059,16 @@ public class AppController implements Initializable {
|
||||
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||
storage.saveWallet(wallet);
|
||||
checkWalletNetwork(wallet);
|
||||
restorePublicKeysFromSeed(wallet, null);
|
||||
restorePublicKeysFromSeed(storage, wallet, null);
|
||||
addWalletTabOrWindow(storage, wallet, null, false);
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
storage.saveWallet(childWallet);
|
||||
checkWalletNetwork(childWallet);
|
||||
restorePublicKeysFromSeed(storage, childWallet, null);
|
||||
addWalletTabOrWindow(storage, childWallet, null, false);
|
||||
}
|
||||
Platform.runLater(() -> selectTab(wallet));
|
||||
} catch(IOException | StorageException | MnemonicException e) {
|
||||
log.error("Error saving imported wallet", e);
|
||||
}
|
||||
@@ -993,8 +1086,17 @@ public class AppController implements Initializable {
|
||||
storage.setEncryptionPubKey(encryptionPubKey);
|
||||
storage.saveWallet(wallet);
|
||||
checkWalletNetwork(wallet);
|
||||
restorePublicKeysFromSeed(wallet, key);
|
||||
restorePublicKeysFromSeed(storage, wallet, key);
|
||||
addWalletTabOrWindow(storage, wallet, null, false);
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
childWallet.encrypt(key);
|
||||
storage.saveWallet(childWallet);
|
||||
checkWalletNetwork(childWallet);
|
||||
restorePublicKeysFromSeed(storage, childWallet, key);
|
||||
addWalletTabOrWindow(storage, childWallet, null, false);
|
||||
}
|
||||
Platform.runLater(() -> selectTab(wallet));
|
||||
} catch(IOException | StorageException | MnemonicException e) {
|
||||
log.error("Error saving imported wallet", e);
|
||||
} finally {
|
||||
@@ -1015,11 +1117,9 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
public void exportWallet(ActionEvent event) {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.WALLET) {
|
||||
WalletTabData walletTabData = (WalletTabData)tabData;
|
||||
WalletExportDialog dlg = new WalletExportDialog(walletTabData.getWallet());
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm.getWallet());
|
||||
Optional<Wallet> wallet = dlg.showAndWait();
|
||||
if(wallet.isPresent()) {
|
||||
//Successful export
|
||||
@@ -1034,10 +1134,9 @@ public class AppController implements Initializable {
|
||||
|
||||
public void signVerifyMessage(ActionEvent event) {
|
||||
MessageSignDialog messageSignDialog = null;
|
||||
Tab tab = tabs.getSelectionModel().getSelectedItem();
|
||||
if(tab != null && tab.getUserData() instanceof WalletTabData) {
|
||||
WalletTabData walletTabData = (WalletTabData)tab.getUserData();
|
||||
Wallet wallet = walletTabData.getWallet();
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
if(wallet.getKeystores().size() == 1 &&
|
||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
|
||||
//Can sign and verify
|
||||
@@ -1054,11 +1153,9 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
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();
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
BitcoinUnit bitcoinUnit = Config.get().getBitcoinUnit();
|
||||
if(bitcoinUnit == BitcoinUnit.AUTO) {
|
||||
bitcoinUnit = wallet.getAutoUnit();
|
||||
@@ -1079,17 +1176,22 @@ public class AppController implements Initializable {
|
||||
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
|
||||
}
|
||||
|
||||
public void lockWallet(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
EventManager.get().post(new WalletLockEvent(selectedWalletForm.getMasterWallet()));
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshWallet(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();
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
Wallet pastWallet = wallet.copy();
|
||||
walletTabData.getStorage().backupTempWallet();
|
||||
selectedWalletForm.getStorage().backupTempWallet();
|
||||
wallet.clearHistory();
|
||||
AppServices.clearTransactionHistoryCache(wallet);
|
||||
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, walletTabData.getWalletForm().getWalletId()));
|
||||
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, selectedWalletForm.getWalletId()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1117,7 +1219,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
public void addWalletTab(Storage storage, Wallet wallet, Wallet backupWallet) {
|
||||
try {
|
||||
if(wallet.isMasterWallet()) {
|
||||
String name = storage.getWalletName(wallet);
|
||||
if(!name.equals(wallet.getName())) {
|
||||
wallet.setName(name);
|
||||
@@ -1130,10 +1232,91 @@ public class AppController implements Initializable {
|
||||
tabLabel.setGraphic(glyph);
|
||||
tabLabel.setGraphicTextGap(5.0);
|
||||
tab.setGraphic(tabLabel);
|
||||
tab.setContextMenu(getTabContextMenu(tab));
|
||||
tab.setClosable(true);
|
||||
tab.setOnCloseRequest(event -> {
|
||||
if(AppServices.getWhirlpoolServices().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) {
|
||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Close mix to wallet?", "This wallet has been configured as the final destination for mixes, and needs to be open for this to occur.\n\nAre you sure you want to close?", ButtonType.NO, ButtonType.YES);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.NO) {
|
||||
event.consume();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TabPane subTabs = new TabPane();
|
||||
subTabs.setSide(Side.RIGHT);
|
||||
setSubTabsVisible(subTabs, false);
|
||||
subTabs.rotateGraphicProperty().set(true);
|
||||
tab.setContent(subTabs);
|
||||
|
||||
WalletForm walletForm = addWalletSubTab(subTabs, storage, wallet, backupWallet);
|
||||
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
|
||||
tab.setUserData(tabData);
|
||||
tab.setContextMenu(getTabContextMenu(tab));
|
||||
walletForm.lockedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setSubTabsVisible(subTabs, !newValue && subTabs.getTabs().size() > 1);
|
||||
});
|
||||
|
||||
subTabs.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedTab) -> {
|
||||
if(selectedTab != null) {
|
||||
EventManager.get().post(new WalletTabSelectedEvent(tab));
|
||||
}
|
||||
});
|
||||
|
||||
subTabs.getTabs().addListener((ListChangeListener<Tab>) c -> {
|
||||
if(c.next() && (c.wasAdded() || c.wasRemoved())) {
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
|
||||
}
|
||||
});
|
||||
|
||||
tabs.getTabs().add(tab);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
} else {
|
||||
for(Tab walletTab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData)walletTab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
WalletTabData walletTabData = (WalletTabData)tabData;
|
||||
if(walletTabData.getWallet() == wallet.getMasterWallet()) {
|
||||
TabPane subTabs = (TabPane)walletTab.getContent();
|
||||
addWalletSubTab(subTabs, storage, wallet, backupWallet);
|
||||
Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0));
|
||||
Label masterLabel = (Label)masterTab.getGraphic();
|
||||
masterLabel.setText(getAutomaticName(wallet.getMasterWallet()));
|
||||
Platform.runLater(() -> {
|
||||
setSubTabsVisible(subTabs, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
EventManager.get().post(new WalletOpenedEvent(storage, wallet));
|
||||
}
|
||||
|
||||
private void setSubTabsVisible(TabPane subTabs, boolean visible) {
|
||||
if(visible) {
|
||||
subTabs.getStyleClass().remove("master-only");
|
||||
if(!subTabs.getStyleClass().contains("wallet-subtabs")) {
|
||||
subTabs.getStyleClass().add("wallet-subtabs");
|
||||
}
|
||||
} else {
|
||||
if(!subTabs.getStyleClass().contains("master-only")) {
|
||||
subTabs.getStyleClass().add("master-only");
|
||||
}
|
||||
subTabs.getStyleClass().remove("wallet-subtabs");
|
||||
}
|
||||
}
|
||||
|
||||
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet, Wallet backupWallet) {
|
||||
try {
|
||||
Tab subTab = new Tab();
|
||||
subTab.setClosable(false);
|
||||
Label subTabLabel = new Label(wallet.isMasterWallet() ? getAutomaticName(wallet) : wallet.getName());
|
||||
subTabLabel.setGraphic(getSubTabGlyph(wallet));
|
||||
subTabLabel.setContentDisplay(ContentDisplay.TOP);
|
||||
subTabLabel.setAlignment(Pos.TOP_CENTER);
|
||||
subTab.setGraphic(subTabLabel);
|
||||
FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml"));
|
||||
tab.setContent(walletLoader.load());
|
||||
subTab.setContent(walletLoader.load());
|
||||
WalletController controller = walletLoader.getController();
|
||||
|
||||
EventManager.get().post(new WalletOpeningEvent(storage, wallet));
|
||||
@@ -1144,15 +1327,57 @@ public class AppController implements Initializable {
|
||||
controller.setWalletForm(walletForm);
|
||||
|
||||
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
|
||||
tab.setUserData(tabData);
|
||||
subTab.setUserData(tabData);
|
||||
|
||||
tabs.getTabs().add(tab);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
subTabs.getTabs().add(subTab);
|
||||
subTabs.getTabs().sort((o1, o2) -> {
|
||||
WalletTabData tabData1 = (WalletTabData) o1.getUserData();
|
||||
WalletTabData tabData2 = (WalletTabData) o2.getUserData();
|
||||
return tabData1.getWallet().compareTo(tabData2.getWallet());
|
||||
});
|
||||
subTabs.getSelectionModel().select(subTab);
|
||||
|
||||
return walletForm;
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Glyph getSubTabGlyph(Wallet wallet) {
|
||||
Glyph tabGlyph;
|
||||
StandardAccount standardAccount = wallet.getStandardAccountType();
|
||||
if(standardAccount == StandardAccount.WHIRLPOOL_PREMIX) {
|
||||
tabGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
} else if(standardAccount == StandardAccount.WHIRLPOOL_POSTMIX) {
|
||||
tabGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.SEND);
|
||||
} else if(standardAccount == StandardAccount.WHIRLPOOL_BADBANK) {
|
||||
tabGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BIOHAZARD);
|
||||
} else {
|
||||
tabGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||
}
|
||||
|
||||
tabGlyph.setFontSize(12);
|
||||
return tabGlyph;
|
||||
}
|
||||
|
||||
private String getAutomaticName(Wallet wallet) {
|
||||
int account = wallet.getAccountIndex();
|
||||
return (account < 0 || account > 9) ? wallet.getName() : (!wallet.isWhirlpoolMasterWallet() || account > 1 ? "Account #" + account : "Deposit");
|
||||
}
|
||||
|
||||
public WalletForm getSelectedWalletForm() {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)selectedTab.getContent();
|
||||
Tab selectedSubTab = subTabs.getSelectionModel().getSelectedItem();
|
||||
WalletTabData subWalletTabData = (WalletTabData)selectedSubTab.getUserData();
|
||||
return subWalletTabData.getWalletForm();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void openExamples(ActionEvent event) {
|
||||
try {
|
||||
addTransactionTab("p2pkh", null, "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac06241559");
|
||||
@@ -1319,6 +1544,18 @@ public class AppController implements Initializable {
|
||||
private ContextMenu getTabContextMenu(Tab tab) {
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
|
||||
if(tab.getUserData() instanceof WalletTabData walletTabData) {
|
||||
MenuItem lock = new MenuItem("Lock");
|
||||
Glyph lockGlyph = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
|
||||
lockGlyph.setFontSize(12);
|
||||
lock.setGraphic(lockGlyph);
|
||||
lock.disableProperty().bind(walletTabData.getWalletForm().lockedProperty());
|
||||
lock.setOnAction(event -> {
|
||||
EventManager.get().post(new WalletLockEvent(walletTabData.getWallet()));
|
||||
});
|
||||
contextMenu.getItems().addAll(lock);
|
||||
}
|
||||
|
||||
MenuItem close = new MenuItem("Close");
|
||||
close.setOnAction(event -> {
|
||||
tabs.getTabs().remove(tab);
|
||||
@@ -1388,7 +1625,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void tabLabelStartAnimation(Wallet wallet) {
|
||||
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((WalletTabData)tab.getUserData()).getWallet() == wallet).forEach(this::tabLabelStartAnimation);
|
||||
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((TabPane)tab.getContent()).getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWallet()).anyMatch(tabWallet -> tabWallet == wallet)).forEach(this::tabLabelStartAnimation);
|
||||
}
|
||||
|
||||
private void tabLabelStartAnimation(Transaction transaction) {
|
||||
@@ -1416,7 +1653,15 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void tabLabelStopAnimation(Wallet wallet) {
|
||||
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((WalletTabData)tab.getUserData()).getWallet() == wallet).forEach(this::tabLabelStopAnimation);
|
||||
Set<Wallet> relatedWallets = new HashSet<>(wallet.isMasterWallet() ? wallet.getChildWallets() : wallet.getMasterWallet().getChildWallets());
|
||||
relatedWallets.remove(wallet);
|
||||
if(!wallet.isMasterWallet()) {
|
||||
relatedWallets.add(wallet.getMasterWallet());
|
||||
}
|
||||
|
||||
if(loadingWallets.stream().noneMatch(relatedWallets::contains)) {
|
||||
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((TabPane)tab.getContent()).getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWallet()).anyMatch(tabWallet -> tabWallet == wallet)).forEach(this::tabLabelStopAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
private void tabLabelStopAnimation(Transaction transaction) {
|
||||
@@ -1438,6 +1683,29 @@ public class AppController implements Initializable {
|
||||
tabLabel.getGraphic().getStyleClass().remove("failure");
|
||||
}
|
||||
|
||||
private void setTorIcon() {
|
||||
TorStatusLabel torStatusLabel = null;
|
||||
for(Node node : statusBar.getRightItems()) {
|
||||
if(node instanceof TorStatusLabel) {
|
||||
torStatusLabel = (TorStatusLabel)node;
|
||||
}
|
||||
}
|
||||
|
||||
if(!AppServices.isUsingProxy()) {
|
||||
if(torStatusLabel != null) {
|
||||
torStatusLabel.update();
|
||||
statusBar.getRightItems().removeAll(torStatusLabel);
|
||||
}
|
||||
} else {
|
||||
if(torStatusLabel == null) {
|
||||
torStatusLabel = new TorStatusLabel();
|
||||
statusBar.getRightItems().add(Math.max(statusBar.getRightItems().size() - 2, 0), torStatusLabel);
|
||||
} else {
|
||||
torStatusLabel.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void themeChanged(ThemeChangedEvent event) {
|
||||
String darkCss = getClass().getResource("darktheme.css").toExternalForm();
|
||||
@@ -1473,6 +1741,7 @@ public class AppController implements Initializable {
|
||||
} else {
|
||||
saveTransaction.setVisible(false);
|
||||
}
|
||||
lockWallet.setDisable(true);
|
||||
exportWallet.setDisable(true);
|
||||
showLoadingLog.setDisable(true);
|
||||
showUtxosChart.setDisable(true);
|
||||
@@ -1482,6 +1751,7 @@ public class AppController implements Initializable {
|
||||
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(true);
|
||||
lockWallet.setDisable(walletTabData.getWalletForm().lockedProperty().get());
|
||||
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
|
||||
showLoadingLog.setDisable(false);
|
||||
showUtxosChart.setDisable(false);
|
||||
@@ -1506,11 +1776,9 @@ public class AppController implements Initializable {
|
||||
|
||||
@Subscribe
|
||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||
Tab tab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
WalletTabData walletTabData = (WalletTabData)tabData;
|
||||
if(walletTabData.getWalletForm().getWalletId().equals(event.getWalletId())) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
||||
exportWallet.setDisable(!event.getWallet().isValid());
|
||||
}
|
||||
}
|
||||
@@ -1519,9 +1787,44 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void newWalletTransactions(NewWalletTransactionsEvent event) {
|
||||
if(Config.get().isNotifyNewTransactions() && getOpenWallets().containsKey(event.getWallet())) {
|
||||
String text;
|
||||
if(event.getBlockTransactions().size() == 1) {
|
||||
BlockTransaction blockTransaction = event.getBlockTransactions().get(0);
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)tab.getContent();
|
||||
for(Tab subTab : subTabs.getTabs()) {
|
||||
TabData tabData = (TabData)subTab.getUserData();
|
||||
if(tabData instanceof WalletTabData walletTabData) {
|
||||
if(walletTabData.getWallet().equals(event.getWallet()) && walletTabData.getWalletForm().lockedProperty().get()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<BlockTransaction> blockTransactions = new ArrayList<>(event.getBlockTransactions());
|
||||
List<BlockTransaction> whirlpoolTransactions = event.getWhirlpoolMixTransactions();
|
||||
blockTransactions.removeAll(whirlpoolTransactions);
|
||||
|
||||
if(!whirlpoolTransactions.isEmpty()) {
|
||||
BlockTransaction blockTransaction = whirlpoolTransactions.get(0);
|
||||
String status;
|
||||
String walletName = event.getWallet().getMasterName() + " " + event.getWallet().getName().toLowerCase();
|
||||
long value = blockTransaction.getTransaction().getOutputs().iterator().next().getValue();
|
||||
long mempoolValue = whirlpoolTransactions.stream().filter(tx -> tx.getHeight() <= 0).mapToLong(tx -> value).sum();
|
||||
long blockchainValue = whirlpoolTransactions.stream().filter(tx -> tx.getHeight() > 0).mapToLong(tx -> value).sum();
|
||||
|
||||
if(mempoolValue > 0) {
|
||||
status = "New " + walletName + " mempool transaction" + (mempoolValue > value ? "s: " : ": ") + event.getValueAsText(mempoolValue);
|
||||
} else {
|
||||
status = "Confirming " + walletName + " transaction" + (blockchainValue > value ? "s: " : ": ") + event.getValueAsText(blockchainValue);
|
||||
}
|
||||
|
||||
statusUpdated(new StatusEvent(status));
|
||||
}
|
||||
|
||||
String text = null;
|
||||
if(blockTransactions.size() == 1) {
|
||||
BlockTransaction blockTransaction = blockTransactions.get(0);
|
||||
if(blockTransaction.getHeight() <= 0) {
|
||||
text = "New mempool transaction: ";
|
||||
} else {
|
||||
@@ -1536,9 +1839,9 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
text += event.getValueAsText(event.getTotalValue());
|
||||
} else {
|
||||
} else if(blockTransactions.size() > 1) {
|
||||
if(event.getTotalBlockchainValue() > 0 && event.getTotalMempoolValue() > 0) {
|
||||
text = "New transactions: " + event.getValueAsText(event.getTotalValue()) + " total (" + event.getValueAsText(event.getTotalMempoolValue()) + " in mempool)";
|
||||
text = "New transactions: " + event.getValueAsText(event.getTotalValue()) + " total";
|
||||
} else if(event.getTotalMempoolValue() > 0) {
|
||||
text = "New mempool transactions: " + event.getValueAsText(event.getTotalMempoolValue()) + " total";
|
||||
} else {
|
||||
@@ -1546,29 +1849,39 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
Window.getWindows().forEach(window -> {
|
||||
String notificationStyles = AppController.class.getResource("notificationpopup.css").toExternalForm();
|
||||
if(!window.getScene().getStylesheets().contains(notificationStyles)) {
|
||||
window.getScene().getStylesheets().add(notificationStyles);
|
||||
if(text != null) {
|
||||
Window.getWindows().forEach(window -> {
|
||||
String notificationStyles = AppController.class.getResource("notificationpopup.css").toExternalForm();
|
||||
if(!window.getScene().getStylesheets().contains(notificationStyles)) {
|
||||
window.getScene().getStylesheets().add(notificationStyles);
|
||||
}
|
||||
});
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
String walletName = event.getWallet().getMasterName();
|
||||
if(walletName.length() > 25) {
|
||||
walletName = walletName.substring(0, 25) + "...";
|
||||
}
|
||||
if(!event.getWallet().isMasterWallet()) {
|
||||
walletName += " " + event.getWallet().getName();
|
||||
}
|
||||
});
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
Notifications notificationBuilder = Notifications.create()
|
||||
.title("Sparrow - " + event.getWallet().getName())
|
||||
.text(text)
|
||||
.graphic(new ImageView(image))
|
||||
.hideAfter(Duration.seconds(15))
|
||||
.position(Pos.TOP_RIGHT)
|
||||
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new ImageView(image)))
|
||||
.onAction(e -> selectTab(event.getWallet()));
|
||||
Notifications notificationBuilder = Notifications.create()
|
||||
.title("Sparrow - " + walletName)
|
||||
.text(text)
|
||||
.graphic(new ImageView(image))
|
||||
.hideAfter(Duration.seconds(15))
|
||||
.position(Pos.TOP_RIGHT)
|
||||
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new ImageView(image)))
|
||||
.onAction(e -> selectTab(event.getWallet()));
|
||||
|
||||
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
|
||||
if(org.controlsfx.tools.Utils.getWindow(null) == null) {
|
||||
notificationBuilder.owner(tabs.getScene().getWindow());
|
||||
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
|
||||
if(org.controlsfx.tools.Utils.getWindow(null) == null) {
|
||||
notificationBuilder.owner(tabs.getScene().getWindow());
|
||||
}
|
||||
|
||||
notificationBuilder.show();
|
||||
}
|
||||
|
||||
notificationBuilder.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1666,6 +1979,7 @@ public class AppController implements Initializable {
|
||||
String status = CONNECTION_FAILED_PREFIX + event.getMessage();
|
||||
statusUpdated(new StatusEvent(status));
|
||||
serverToggleStopAnimation();
|
||||
setTorIcon();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1674,6 +1988,7 @@ public class AppController implements Initializable {
|
||||
statusUpdated(new StatusEvent(status));
|
||||
setServerToggleTooltip(event.getBlockHeight());
|
||||
serverToggleStopAnimation();
|
||||
setTorIcon();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1693,7 +2008,8 @@ public class AppController implements Initializable {
|
||||
|
||||
@Subscribe
|
||||
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
||||
if(event.getClosedWalletTabData().stream().map(WalletTabData::getWallet).anyMatch(loadingWallets::remove) && loadingWallets.isEmpty()) {
|
||||
event.getClosedWalletTabData().stream().map(WalletTabData::getWallet).forEach(loadingWallets::remove);
|
||||
if(event.getClosedWalletTabData().stream().map(WalletTabData::getWallet).anyMatch(emptyLoadingWallets::remove) && emptyLoadingWallets.isEmpty()) {
|
||||
if(statusBar.getText().equals(LOADING_TRANSACTIONS_MESSAGE)) {
|
||||
statusBar.setText("");
|
||||
}
|
||||
@@ -1719,9 +2035,10 @@ public class AppController implements Initializable {
|
||||
statusUpdated(new StatusEvent(LOADING_TRANSACTIONS_MESSAGE, 120));
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(-1);
|
||||
loadingWallets.add(event.getWallet());
|
||||
emptyLoadingWallets.add(event.getWallet());
|
||||
}
|
||||
}
|
||||
loadingWallets.add(event.getWallet());
|
||||
tabLabelStartAnimation(event.getWallet());
|
||||
}
|
||||
}
|
||||
@@ -1735,8 +2052,9 @@ public class AppController implements Initializable {
|
||||
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
|
||||
statusBar.setProgress(0);
|
||||
}
|
||||
tabLabelStopAnimation(event.getWallet());
|
||||
emptyLoadingWallets.remove(event.getWallet());
|
||||
loadingWallets.remove(event.getWallet());
|
||||
tabLabelStopAnimation(event.getWallet());
|
||||
tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData && ((WalletTabData)tab.getUserData()).getWallet() == event.getWallet()).forEach(this::tabLabelRemoveFailure);
|
||||
}
|
||||
}
|
||||
@@ -1797,18 +2115,21 @@ public class AppController implements Initializable {
|
||||
public void torBootStatus(TorBootStatusEvent event) {
|
||||
serverToggle.setDisable(true);
|
||||
statusUpdated(new StatusEvent(event.getStatus(), 120));
|
||||
setTorIcon();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void torFailedStatus(TorFailedStatusEvent event) {
|
||||
serverToggle.setDisable(false);
|
||||
statusUpdated(new StatusEvent(event.getStatus()));
|
||||
setTorIcon();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void torReadyStatus(TorReadyStatusEvent event) {
|
||||
serverToggle.setDisable(false);
|
||||
statusUpdated(new StatusEvent(event.getStatus()));
|
||||
setTorIcon();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1826,23 +2147,29 @@ public class AppController implements Initializable {
|
||||
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;
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)tab.getContent();
|
||||
for(Tab subTab : subTabs.getTabs()) {
|
||||
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
|
||||
if(event.getStorage().getWalletId(event.getWallet()).equals(walletTabData.getWalletForm().getWalletId())) {
|
||||
tabs.getSelectionModel().select(tab);
|
||||
subTabs.getSelectionModel().select(subTab);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData) tab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.WALLET) {
|
||||
WalletTabData walletTabData = (WalletTabData) tabData;
|
||||
if(event.getStorage().getWalletFile().equals(walletTabData.getStorage().getWalletFile())) {
|
||||
tabs.getSelectionModel().select(tab);
|
||||
return;
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)tab.getContent();
|
||||
for(Tab subTab : subTabs.getTabs()) {
|
||||
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
|
||||
if(event.getStorage().getWalletFile().equals(walletTabData.getStorage().getWalletFile())) {
|
||||
tabs.getSelectionModel().select(tab);
|
||||
subTabs.getSelectionModel().select(subTab);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1922,4 +2249,30 @@ public class AppController implements Initializable {
|
||||
public void recieveAction(ReceiveActionEvent event) {
|
||||
selectTab(event.getWallet());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void childWalletAdded(ChildWalletAddedEvent event) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(event.getWallet());
|
||||
if(storage == null) {
|
||||
throw new IllegalStateException("Cannot find storage for master wallet");
|
||||
}
|
||||
|
||||
addWalletTab(storage, event.getChildWallet(), null);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletLock(WalletLockEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
|
||||
lockWallet.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletUnlock(WalletUnlockEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
|
||||
lockWallet.setDisable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ 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.BlockHeader;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
@@ -15,12 +16,14 @@ import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Node;
|
||||
@@ -45,6 +48,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.awt.*;
|
||||
import java.awt.desktop.OpenFilesHandler;
|
||||
import java.awt.desktop.OpenURIHandler;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
@@ -71,6 +75,8 @@ public class AppServices {
|
||||
|
||||
private static AppServices INSTANCE;
|
||||
|
||||
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
|
||||
|
||||
private final MainApp application;
|
||||
|
||||
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
|
||||
@@ -89,8 +95,12 @@ public class AppServices {
|
||||
|
||||
private TorService torService;
|
||||
|
||||
private ScheduledService<Void> preventSleepService;
|
||||
|
||||
private static Integer currentBlockHeight;
|
||||
|
||||
private static BlockHeader latestBlockHeader;
|
||||
|
||||
private static Map<Integer, Double> targetBlockFeeRates;
|
||||
|
||||
private static final Map<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
||||
@@ -112,6 +122,11 @@ public class AppServices {
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean online) {
|
||||
if(online) {
|
||||
if(Config.get().requiresInternalTor() && !isTorRunning()) {
|
||||
if(torService.getState() == Worker.State.SCHEDULED) {
|
||||
torService.cancel();
|
||||
torService.reset();
|
||||
}
|
||||
|
||||
torService.start();
|
||||
} else {
|
||||
restartServices();
|
||||
@@ -135,6 +150,7 @@ public class AppServices {
|
||||
public AppServices(MainApp application) {
|
||||
this.application = application;
|
||||
EventManager.get().register(this);
|
||||
EventManager.get().register(whirlpoolServices);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -143,6 +159,7 @@ public class AppServices {
|
||||
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
|
||||
versionCheckService = createVersionCheckService();
|
||||
torService = createTorService();
|
||||
preventSleepService = createPreventSleepService();
|
||||
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
|
||||
@@ -170,6 +187,10 @@ public class AppServices {
|
||||
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET) {
|
||||
restartService(versionCheckService);
|
||||
}
|
||||
|
||||
if(config.isPreventSleep()) {
|
||||
restartService(preventSleepService);
|
||||
}
|
||||
}
|
||||
|
||||
private void restartService(ScheduledService<?> service) {
|
||||
@@ -369,6 +390,39 @@ public class AppServices {
|
||||
return torService;
|
||||
}
|
||||
|
||||
private ScheduledService<Void> createPreventSleepService() {
|
||||
ScheduledService<Void> preventSleepService = new ScheduledService<Void>() {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
protected Void call() {
|
||||
try {
|
||||
Robot robot = new Robot();
|
||||
robot.keyRelease(KeyEvent.VK_F16);
|
||||
} catch(Exception e) {
|
||||
log.debug("Error preventing sleep", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
preventSleepService.setPeriod(Duration.minutes(1));
|
||||
return preventSleepService;
|
||||
}
|
||||
|
||||
public void setPreventSleep(boolean preventSleep) {
|
||||
if(preventSleepService != null) {
|
||||
if(preventSleep) {
|
||||
restartService(preventSleepService);
|
||||
} else {
|
||||
preventSleepService.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isTorRunning() {
|
||||
return Tor.getDefault() != null;
|
||||
}
|
||||
@@ -413,6 +467,10 @@ public class AppServices {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static WhirlpoolServices getWhirlpoolServices() {
|
||||
return get().whirlpoolServices;
|
||||
}
|
||||
|
||||
public static AppController newAppWindow(Stage stage) {
|
||||
try {
|
||||
FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml"));
|
||||
@@ -473,6 +531,10 @@ public class AppServices {
|
||||
return openWallets;
|
||||
}
|
||||
|
||||
public Wallet getWallet(String walletId) {
|
||||
return getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -503,6 +565,10 @@ public class AppServices {
|
||||
return currentBlockHeight;
|
||||
}
|
||||
|
||||
public static BlockHeader getLatestBlockHeader() {
|
||||
return latestBlockHeader;
|
||||
}
|
||||
|
||||
public static Map<Integer, Double> getTargetBlockFeeRates() {
|
||||
return targetBlockFeeRates;
|
||||
}
|
||||
@@ -773,9 +839,11 @@ public class AppServices {
|
||||
@Subscribe
|
||||
public void newConnection(ConnectionEvent event) {
|
||||
currentBlockHeight = event.getBlockHeight();
|
||||
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -786,6 +854,8 @@ public class AppServices {
|
||||
@Subscribe
|
||||
public void newBlock(NewBlockEvent event) {
|
||||
currentBlockHeight = event.getHeight();
|
||||
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
String status = "Updating to new block height " + event.getHeight();
|
||||
EventManager.get().post(new StatusEvent(status));
|
||||
}
|
||||
@@ -848,7 +918,7 @@ public class AppServices {
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if(!Window.getWindows().isEmpty()) {
|
||||
List<File> walletFiles = allWallets.stream().filter(walletTabData -> walletTabData.getWallet().getMasterWallet() == null).map(walletTabData -> walletTabData.getStorage().getWalletFile()).collect(Collectors.toList());
|
||||
List<File> walletFiles = allWallets.stream().filter(walletTabData -> walletTabData.getWallet().getMasterWallet() == null).map(walletTabData -> walletTabData.getStorage().getWalletFile()).filter(File::exists).collect(Collectors.toList());
|
||||
Config.get().setRecentWalletFiles(Config.get().isLoadRecentWallets() ? walletFiles : Collections.emptyList());
|
||||
}
|
||||
});
|
||||
@@ -921,7 +991,7 @@ public class AppServices {
|
||||
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
|
||||
onlineProperty.set(false);
|
||||
log.info("Failed to fetch wallet history from " + Config.get().getServerAddress() + ", reconnecting to another server...");
|
||||
log.warn("Failed to fetch wallet history from " + Config.get().getServerAddress() + ", reconnecting to another server...");
|
||||
Config.get().changePublicServer();
|
||||
onlineProperty.set(true);
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import java.util.stream.Collectors;
|
||||
public class MainApp extends Application {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.4.2";
|
||||
public static final String APP_VERSION = "1.5.1";
|
||||
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";
|
||||
@@ -42,7 +42,13 @@ public class MainApp extends Application {
|
||||
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e));
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||
if(e instanceof IndexOutOfBoundsException && Arrays.stream(e.getStackTrace()).anyMatch(element -> element.getClassName().equals("javafx.scene.chart.BarChart"))) {
|
||||
LoggerFactory.getLogger(MainApp.class).debug("Exception in thread \"" + t.getName() + "\"", e);;
|
||||
} else {
|
||||
LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e);
|
||||
}
|
||||
});
|
||||
super.init();
|
||||
}
|
||||
|
||||
@@ -64,7 +70,6 @@ public class MainApp extends Application {
|
||||
if(optionalMode.isPresent()) {
|
||||
mode = optionalMode.get();
|
||||
Config.get().setMode(mode);
|
||||
Config.get().setCoreWallet(Bwt.DEFAULT_CORE_WALLET);
|
||||
|
||||
if(mode.equals(Mode.ONLINE)) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
|
||||
@@ -79,9 +84,6 @@ public class MainApp extends Application {
|
||||
|
||||
if(Config.get().getServerType() == null && Config.get().getCoreServer() == null && Config.get().getElectrumServer() != null) {
|
||||
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
|
||||
} else if(Config.get().getServerType() == ServerType.BITCOIN_CORE && Config.get().getCoreWallet() == null) {
|
||||
Config.get().setCoreMultiWallet(Boolean.TRUE);
|
||||
Config.get().setCoreWallet("");
|
||||
}
|
||||
|
||||
if(Config.get().getHdCapture() == null && Platform.getCurrent() == Platform.OSX) {
|
||||
@@ -90,6 +92,11 @@ public class MainApp extends Application {
|
||||
|
||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
|
||||
|
||||
if(Config.get().getAppHeight() != null && Config.get().getAppWidth() != null) {
|
||||
mainStage.setWidth(Config.get().getAppWidth());
|
||||
mainStage.setHeight(Config.get().getAppHeight());
|
||||
}
|
||||
|
||||
AppController appController = AppServices.newAppWindow(stage);
|
||||
|
||||
if(createNewWallet) {
|
||||
@@ -119,6 +126,8 @@ public class MainApp extends Application {
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
AppServices.get().stop();
|
||||
Config.get().setAppWidth(mainStage.getWidth());
|
||||
Config.get().setAppHeight(mainStage.getHeight());
|
||||
mainStage.close();
|
||||
if(sparrowInstance != null) {
|
||||
sparrowInstance.freeLock();
|
||||
|
||||
@@ -2,11 +2,16 @@ package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.LogHandler;
|
||||
import com.sparrowwallet.sparrow.event.TorStatusEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
public class TorLogHandler implements LogHandler {
|
||||
private static final Logger log = LoggerFactory.getLogger(TorLogHandler.class);
|
||||
|
||||
@Override
|
||||
public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) {
|
||||
log.debug(message);
|
||||
EventManager.get().post(new TorStatusEvent(message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AddAccountDialog extends Dialog<StandardAccount> {
|
||||
private final ComboBox<StandardAccount> standardAccountCombo;
|
||||
|
||||
public AddAccountDialog(Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("Add Account");
|
||||
dialogPane.setHeaderText("Choose an account to add:");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(200);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SORT_NUMERIC_DOWN);
|
||||
key.setFontSize(50);
|
||||
dialogPane.setGraphic(key);
|
||||
|
||||
final VBox content = new VBox(10);
|
||||
content.setPrefHeight(50);
|
||||
|
||||
standardAccountCombo = new ComboBox<>();
|
||||
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
List<Integer> existingIndexes = new ArrayList<>();
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
existingIndexes.add(masterWallet.getAccountIndex());
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
existingIndexes.add(childWallet.getAccountIndex());
|
||||
}
|
||||
|
||||
List<StandardAccount> availableAccounts = new ArrayList<>();
|
||||
for(StandardAccount standardAccount : StandardAccount.values()) {
|
||||
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
|
||||
availableAccounts.add(standardAccount);
|
||||
}
|
||||
}
|
||||
|
||||
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX);
|
||||
}
|
||||
|
||||
standardAccountCombo.setItems(FXCollections.observableList(availableAccounts));
|
||||
standardAccountCombo.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(StandardAccount account) {
|
||||
if(account == null) {
|
||||
return "None Available";
|
||||
}
|
||||
|
||||
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(account)) {
|
||||
return "Whirlpool Accounts";
|
||||
}
|
||||
|
||||
return account.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StandardAccount fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if(standardAccountCombo.getItems().isEmpty()) {
|
||||
Button okButton = (Button) dialogPane.lookupButton(ButtonType.OK);
|
||||
okButton.setDisable(true);
|
||||
} else {
|
||||
standardAccountCombo.getSelectionModel().select(0);
|
||||
}
|
||||
content.getChildren().add(standardAccountCombo);
|
||||
|
||||
dialogPane.setContent(content);
|
||||
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? standardAccountCombo.getValue() : null);
|
||||
}
|
||||
}
|
||||
@@ -11,55 +11,44 @@ import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
public class AddressCell extends TreeTableCell<Entry, Entry> {
|
||||
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
public AddressCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setContentDisplay(ContentDisplay.RIGHT);
|
||||
getStyleClass().add("address-cell");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Entry entry, boolean empty) {
|
||||
super.updateItem(entry, empty);
|
||||
protected void updateItem(UtxoEntry.AddressStatus addressStatus, boolean empty) {
|
||||
super.updateItem(addressStatus, empty);
|
||||
|
||||
EntryCell.applyRowStyles(this, entry);
|
||||
getStyleClass().add("address-cell");
|
||||
UtxoEntry utxoEntry = addressStatus == null ? null : addressStatus.getUtxoEntry();
|
||||
EntryCell.applyRowStyles(this, utxoEntry);
|
||||
|
||||
if (empty) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
if(entry instanceof UtxoEntry) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
Address address = utxoEntry.getAddress();
|
||||
if(utxoEntry != null) {
|
||||
Address address = addressStatus.getAddress();
|
||||
setText(address.toString());
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(getTooltipText(utxoEntry));
|
||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
|
||||
setTooltip(tooltip);
|
||||
|
||||
if(utxoEntry.isDuplicateAddress()) {
|
||||
if(addressStatus.isDuplicate()) {
|
||||
setGraphic(getDuplicateGlyph());
|
||||
} else {
|
||||
setGraphic(null);
|
||||
}
|
||||
|
||||
utxoEntry.duplicateAddressProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue) {
|
||||
setGraphic(getDuplicateGlyph());
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText(getTooltipText(utxoEntry));
|
||||
setTooltip(tt);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getTooltipText(UtxoEntry utxoEntry) {
|
||||
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (utxoEntry.isDuplicateAddress() ? " (Duplicate address)" : "");
|
||||
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
|
||||
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (duplicate ? " (Duplicate address)" : "");
|
||||
}
|
||||
|
||||
public static Glyph getDuplicateGlyph() {
|
||||
|
||||
@@ -34,7 +34,7 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
addressCol.setSortable(false);
|
||||
getColumns().add(addressCol);
|
||||
|
||||
if(address != null) {
|
||||
if(address != null && !rootEntry.getWallet().isWhirlpoolChildWallet()) {
|
||||
addressCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
|
||||
}
|
||||
|
||||
@@ -68,21 +68,23 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
}
|
||||
}
|
||||
|
||||
setOnMouseClicked(mouseEvent -> {
|
||||
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
|
||||
if(mouseEvent.getClickCount() == 2) {
|
||||
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
|
||||
if(treeItem != null && treeItem.getChildren().isEmpty()) {
|
||||
Entry entry = getSelectionModel().getSelectedItem().getValue();
|
||||
if(entry instanceof NodeEntry) {
|
||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
if(!rootEntry.getWallet().isWhirlpoolChildWallet()) {
|
||||
setOnMouseClicked(mouseEvent -> {
|
||||
if(mouseEvent.getButton().equals(MouseButton.PRIMARY)){
|
||||
if(mouseEvent.getClickCount() == 2) {
|
||||
TreeItem<Entry> treeItem = getSelectionModel().getSelectedItem();
|
||||
if(treeItem != null && treeItem.getChildren().isEmpty()) {
|
||||
Entry entry = getSelectionModel().getSelectedItem().getValue();
|
||||
if(entry instanceof NodeEntry) {
|
||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
rootEntry.getChildren().addListener((ListChangeListener<Entry>) c -> {
|
||||
this.refresh();
|
||||
|
||||
@@ -34,11 +34,15 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||
if(entry instanceof UtxoEntry) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
if(utxoEntry.getHashIndex().getHeight() <= 0) {
|
||||
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)"));
|
||||
} else {
|
||||
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
|
||||
setContextMenu(null);
|
||||
} else if(utxoEntry.getHashIndex().getDate() != null) {
|
||||
String date = DATE_FORMAT.format(utxoEntry.getHashIndex().getDate());
|
||||
setText(date);
|
||||
setContextMenu(new DateContextMenu(date, utxoEntry.getHashIndex()));
|
||||
} else {
|
||||
setText("Unknown");
|
||||
setContextMenu(null);
|
||||
}
|
||||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
|
||||
@@ -17,6 +17,10 @@ public class DateLabel extends CopyableLabel {
|
||||
}
|
||||
|
||||
public static String getShortDateFormat(Date date) {
|
||||
if(date == null) {
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
long elapsed = (now.getTime() - date.getTime()) / 1000;
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
private Button unlockButton;
|
||||
private Button enterPinButton;
|
||||
private Button setPassphraseButton;
|
||||
private SplitMenuButton importButton;
|
||||
private ButtonBase importButton;
|
||||
private Button signButton;
|
||||
private Button displayAddressButton;
|
||||
private Button signMessageButton;
|
||||
@@ -61,13 +61,13 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
private boolean defaultDevice;
|
||||
|
||||
public DevicePane(Wallet wallet, Device device, boolean defaultDevice) {
|
||||
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.IMPORT;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.keyDerivation = requiredDerivation;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
@@ -199,35 +199,39 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void createImportButton() {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton = keyDerivation == null ? new SplitMenuButton() : new Button();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation());
|
||||
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
importKeystore(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation());
|
||||
});
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
|
||||
for(ScriptType scriptType : scriptTypes) {
|
||||
MenuItem item = new MenuItem(scriptType.getDescription());
|
||||
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
|
||||
item.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importButton.getItems().add(item);
|
||||
}
|
||||
} else {
|
||||
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importButton.getItems().add(item);
|
||||
|
||||
if(importButton instanceof SplitMenuButton importMenuButton) {
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH};
|
||||
for(ScriptType scriptType : scriptTypes) {
|
||||
MenuItem item = new MenuItem(scriptType.getDescription());
|
||||
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
|
||||
item.setOnAction(event -> {
|
||||
importMenuButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importMenuButton.getItems().add(item);
|
||||
}
|
||||
} else {
|
||||
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
importMenuButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importMenuButton.getItems().add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
@@ -430,7 +434,9 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
setExpanded(true);
|
||||
} else {
|
||||
showOperationButton();
|
||||
setContent(getTogglePassphraseOn());
|
||||
if(!deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||
setContent(getTogglePassphraseOn());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setError("Incorrect PIN", null);
|
||||
@@ -546,7 +552,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);
|
||||
@@ -622,7 +628,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
importButton.setVisible(true);
|
||||
showHideLink.setText("Show derivation...");
|
||||
showHideLink.setVisible(true);
|
||||
setContent(getDerivationEntry(wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation()));
|
||||
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
|
||||
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
|
||||
signButton.setDefaultButton(defaultDevice);
|
||||
signButton.setVisible(true);
|
||||
@@ -642,6 +649,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||
derivationField.setDisable(keyDerivation != null);
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
@@ -651,7 +659,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
|
||||
Button importDerivationButton = new Button("Import");
|
||||
Button importDerivationButton = new Button("Import Custom Derivation");
|
||||
importDerivationButton.setDisable(true);
|
||||
importDerivationButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
@@ -660,7 +669,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
});
|
||||
|
||||
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue));
|
||||
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
@@ -118,8 +119,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
});
|
||||
actionBox.getChildren().add(receiveButton);
|
||||
|
||||
if(nodeEntry.getWallet().getKeystores().size() == 1 &&
|
||||
(nodeEntry.getWallet().getKeystores().get(0).hasPrivateKey() || nodeEntry.getWallet().getKeystores().get(0).getSource() == KeystoreSource.HW_USB)) {
|
||||
if(canSignMessage(nodeEntry.getWallet())) {
|
||||
Button signMessageButton = new Button("");
|
||||
signMessageButton.setGraphic(getSignMessageGlyph());
|
||||
signMessageButton.setOnAction(event -> {
|
||||
@@ -127,10 +127,15 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
messageSignDialog.showAndWait();
|
||||
});
|
||||
actionBox.getChildren().add(signMessageButton);
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
|
||||
if(nodeEntry.getWallet().isWhirlpoolChildWallet()) {
|
||||
setText(address.toString().substring(0, 20) + "...");
|
||||
setContextMenu(null);
|
||||
setGraphic(new HBox());
|
||||
}
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||
setText(hashIndexEntry.getDescription());
|
||||
@@ -268,6 +273,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
|
||||
}
|
||||
|
||||
private static boolean canSignMessage(Wallet wallet) {
|
||||
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
|
||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB);
|
||||
}
|
||||
|
||||
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||
return transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
@@ -292,14 +302,28 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(hashIndexEntry.getWallet(), spendingUtxos)));
|
||||
}
|
||||
|
||||
private static void freezeUtxo(HashIndexEntry hashIndexEntry) {
|
||||
hashIndexEntry.getHashIndex().setStatus(Status.FROZEN);
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
|
||||
private static void freezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
|
||||
.map(tp -> tp.getTreeItem().getValue())
|
||||
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||
.map(e -> ((HashIndexEntry)e).getHashIndex())
|
||||
.filter(ref -> ref.getStatus() != Status.FROZEN)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
utxos.forEach(ref -> ref.setStatus(Status.FROZEN));
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
|
||||
}
|
||||
|
||||
private static void unfreezeUtxo(HashIndexEntry hashIndexEntry) {
|
||||
hashIndexEntry.getHashIndex().setStatus(null);
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), hashIndexEntry.getHashIndex()));
|
||||
private static void unfreezeUtxo(TreeTableView<Entry> treeTableView, HashIndexEntry hashIndexEntry) {
|
||||
List<BlockTransactionHashIndex> utxos = treeTableView.getSelectionModel().getSelectedCells().stream()
|
||||
.map(tp -> tp.getTreeItem().getValue())
|
||||
.filter(e -> e instanceof HashIndexEntry && ((HashIndexEntry)e).getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||
.map(e -> ((HashIndexEntry)e).getHashIndex())
|
||||
.filter(ref -> ref.getStatus() == Status.FROZEN)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
utxos.forEach(ref -> ref.setStatus(null));
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
|
||||
}
|
||||
|
||||
private static Glyph getViewTransactionGlyph() {
|
||||
@@ -449,7 +473,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
});
|
||||
getItems().add(receiveToAddress);
|
||||
|
||||
if(nodeEntry != null) {
|
||||
if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) {
|
||||
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
||||
signVerifyMessage.setGraphic(getSignMessageGlyph());
|
||||
signVerifyMessage.setOnAction(AE -> {
|
||||
@@ -514,7 +538,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
freezeUtxo.setGraphic(getFreezeGlyph());
|
||||
freezeUtxo.setOnAction(AE -> {
|
||||
hide();
|
||||
freezeUtxo(hashIndexEntry);
|
||||
freezeUtxo(treeTableView, hashIndexEntry);
|
||||
});
|
||||
getItems().add(freezeUtxo);
|
||||
} else {
|
||||
@@ -522,7 +546,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
unfreezeUtxo.setGraphic(getUnfreezeGlyph());
|
||||
unfreezeUtxo.setOnAction(AE -> {
|
||||
hide();
|
||||
unfreezeUtxo(hashIndexEntry);
|
||||
unfreezeUtxo(treeTableView, hashIndexEntry);
|
||||
});
|
||||
getItems().add(unfreezeUtxo);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -12,11 +13,13 @@ import java.io.*;
|
||||
public class FileKeystoreImportPane extends FileImportPane {
|
||||
protected final Wallet wallet;
|
||||
private final KeystoreFileImport importer;
|
||||
private final KeyDerivation requiredDerivation;
|
||||
|
||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer) {
|
||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
}
|
||||
|
||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||
@@ -25,6 +28,10 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
|
||||
}
|
||||
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
|
||||
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + keystore.getKeyDerivation().getDerivationPath() + ".");
|
||||
} else {
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,9 +80,11 @@ 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 instanceof Sparrow ? "" : "-" + exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "")) +
|
||||
(extension == null || extension.isEmpty() ? "" : "." + extension));
|
||||
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "");
|
||||
if(exporter instanceof Sparrow) {
|
||||
fileName = wallet.getMasterName();
|
||||
}
|
||||
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
@@ -94,7 +96,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
private void exportWallet(File file) {
|
||||
if(wallet.isEncrypted() && exporter.walletExportRequiresDecryption()) {
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
|
||||
|
||||
@@ -2,13 +2,13 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Bounds;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
@@ -19,6 +19,7 @@ public class HelpLabel extends Label {
|
||||
super("", getHelpGlyph());
|
||||
tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(helpTextProperty());
|
||||
tooltip.graphicProperty().bind(helpGraphicProperty());
|
||||
tooltip.setShowDuration(Duration.seconds(15));
|
||||
getStyleClass().add("help-label");
|
||||
|
||||
@@ -26,10 +27,10 @@ public class HelpLabel extends Label {
|
||||
}
|
||||
|
||||
private static Glyph getHelpGlyph() {
|
||||
Glyph lockGlyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.QUESTION_CIRCLE);
|
||||
lockGlyph.getStyleClass().add("help-icon");
|
||||
lockGlyph.setFontSize(12);
|
||||
return lockGlyph;
|
||||
Glyph glyph = new Glyph("Font Awesome 5 Free Solid", FontAwesome5.Glyph.QUESTION_CIRCLE);
|
||||
glyph.getStyleClass().add("help-icon");
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
|
||||
public final StringProperty helpTextProperty() {
|
||||
@@ -49,4 +50,18 @@ public class HelpLabel extends Label {
|
||||
public final String getHelpText() {
|
||||
return helpText == null ? "" : helpText.getValue();
|
||||
}
|
||||
|
||||
public ObjectProperty<Node> helpGraphicProperty() {
|
||||
if(helpGraphicProperty == null) {
|
||||
helpGraphicProperty = new SimpleObjectProperty<Node>(this, "helpGraphic", null);
|
||||
}
|
||||
|
||||
return helpGraphicProperty;
|
||||
}
|
||||
|
||||
private ObjectProperty<Node> helpGraphicProperty;
|
||||
|
||||
public final void setHelpGraphic(Node graphic) {
|
||||
helpGraphicProperty().setValue(graphic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import javafx.application.Platform;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
@@ -20,11 +21,15 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||
}
|
||||
|
||||
public KeystorePassphraseDialog(String walletName, Keystore keystore) {
|
||||
this(walletName, keystore, false);
|
||||
}
|
||||
|
||||
public KeystorePassphraseDialog(String walletName, Keystore keystore, boolean confirm) {
|
||||
this.passphrase = (CustomPasswordField) TextFields.createClearablePasswordField();
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("Keystore Passphrase" + (walletName != null ? " - " + walletName : ""));
|
||||
dialogPane.setHeaderText("Please enter the passphrase for keystore: \n" + keystore.getLabel());
|
||||
dialogPane.setHeaderText((confirm ? "Re-enter" : "Enter") + " the BIP39 passphrase\n" + (confirm ? "to confirm:" : "for keystore: " + keystore.getLabel()));
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
|
||||
@@ -40,6 +45,13 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||
content.setPrefHeight(50);
|
||||
content.getChildren().add(passphrase);
|
||||
|
||||
Glyph warnGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
warnGlyph.getStyleClass().add("warn-icon");
|
||||
warnGlyph.setFontSize(12);
|
||||
Label warnLabel = new Label("A BIP39 passphrase is not a wallet password!", warnGlyph);
|
||||
warnLabel.setGraphicTextGap(5);
|
||||
content.getChildren().add(warnLabel);
|
||||
|
||||
dialogPane.setContent(content);
|
||||
Platform.runLater(passphrase::requestFocus);
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import java.util.stream.Collectors;
|
||||
public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
private static final DateFormat dateFormatter = new SimpleDateFormat("HH:mm");
|
||||
public static final int MAX_PERIOD_HOURS = 2;
|
||||
private static final double Y_VALUE_BREAK_MVB = 3.0;
|
||||
|
||||
private Tooltip tooltip;
|
||||
|
||||
@@ -68,19 +69,6 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
categoryAxis.setTickLabelRotation(0);
|
||||
|
||||
NumberAxis numberAxis = (NumberAxis)getYAxis();
|
||||
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
long vSizeBytes = object.longValue();
|
||||
return (vSizeBytes / (1000 * 1000)) + " MvB";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
this.setOnMouseMoved(mouseEvent -> {
|
||||
Point2D sceneCoords = this.localToScene(mouseEvent.getX(), mouseEvent.getY());
|
||||
String category = categoryAxis.getValueForDisplay(categoryAxis.sceneToLocal(sceneCoords).getX());
|
||||
@@ -116,6 +104,24 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
previousFeeRate = feeRate;
|
||||
}
|
||||
|
||||
final double maxMvB = getMaxMvB(getData());
|
||||
numberAxis.setTickLabelFormatter(new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
long vSizeBytes = object.longValue();
|
||||
if(maxMvB > Y_VALUE_BREAK_MVB) {
|
||||
return (vSizeBytes / (1000 * 1000)) + " MvB";
|
||||
} else {
|
||||
return (vSizeBytes / (1000)) + " kvB";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if(categories.keySet().iterator().hasNext()) {
|
||||
String time = categories.values().iterator().next();
|
||||
tooltip.setGraphic(new ChartTooltip(time, time, getData()));
|
||||
@@ -159,20 +165,42 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
return categories;
|
||||
}
|
||||
|
||||
private static double getMaxMvB(List<Series<String, Number>> seriesList) {
|
||||
double maxTotal = 0d;
|
||||
for(Series<String, Number> series : seriesList) {
|
||||
maxTotal = Math.max(maxTotal, getMaxMvB(series));
|
||||
}
|
||||
|
||||
return maxTotal;
|
||||
}
|
||||
|
||||
private static double getMaxMvB(Series<String, Number> series) {
|
||||
double total = 0d;
|
||||
for(XYChart.Data<String, Number> data : series.getData()) {
|
||||
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
|
||||
total += mvb;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static class ChartTooltip extends VBox {
|
||||
public ChartTooltip(String category, String time, List<Series<String, Number>> seriesList) {
|
||||
Label title = new Label("At " + time);
|
||||
HBox titleBox = new HBox(title);
|
||||
title.setStyle("-fx-alignment: center; -fx-font-size: 12px; -fx-padding: 0 0 5 0;");
|
||||
getChildren().add(titleBox);
|
||||
double maxMvB = getMaxMvB(seriesList);
|
||||
|
||||
for(int i = seriesList.size() - 1; i >= 0; i--) {
|
||||
Series<String, Number> series = seriesList.get(i);
|
||||
for(XYChart.Data<String, Number> data : series.getData()) {
|
||||
if(data.getXValue().equals(category)) {
|
||||
double mvb = data.getYValue().doubleValue() / (1000 * 1000);
|
||||
if(mvb >= 0.01) {
|
||||
Label label = new Label(series.getName() + ": " + String.format("%.2f", mvb) + " MvB");
|
||||
double kvb = data.getYValue().doubleValue() / 1000;
|
||||
double mvb = kvb / 1000;
|
||||
if(mvb >= 0.01 || (maxMvB < Y_VALUE_BREAK_MVB && mvb > 0.001)) {
|
||||
String amount = (maxMvB < Y_VALUE_BREAK_MVB ? (int)kvb + " kvB" : String.format("%.2f", mvb) + " MvB");
|
||||
Label label = new Label(series.getName() + ": " + amount);
|
||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CIRCLE);
|
||||
if(i < 8) {
|
||||
circle.setStyle("-fx-text-fill: CHART_COLOR_" + (i+1));
|
||||
|
||||
@@ -51,6 +51,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private final Wallet wallet;
|
||||
private WalletNode walletNode;
|
||||
private boolean electrumSignatureFormat;
|
||||
private boolean closed;
|
||||
|
||||
/**
|
||||
* Verification only constructor
|
||||
@@ -177,7 +178,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
ButtonType signButtonType = new javafx.scene.control.ButtonType("Sign", ButtonBar.ButtonData.BACK_PREVIOUS);
|
||||
ButtonType verifyButtonType = new javafx.scene.control.ButtonType("Verify", ButtonBar.ButtonData.NEXT_FORWARD);
|
||||
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
|
||||
ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
if(buttons.length > 0) {
|
||||
dialogPane.getButtonTypes().addAll(buttons);
|
||||
@@ -240,9 +241,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
EventManager.get().unregister(this);
|
||||
if(!closed) {
|
||||
EventManager.get().unregister(this);
|
||||
closed = true;
|
||||
}
|
||||
});
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||
|
||||
@@ -277,8 +282,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
private boolean isValidAddress() {
|
||||
try {
|
||||
getAddress();
|
||||
return true;
|
||||
Address address = getAddress();
|
||||
return address.getScriptType() != ScriptType.P2TR;
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -311,7 +316,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||
String signatureText = privKey.signMessage(message.getText().trim(), scriptType, null);
|
||||
String signatureText = privKey.signMessage(message.getText().trim(), scriptType);
|
||||
signature.clear();
|
||||
signature.appendText(signatureText);
|
||||
privKey.clear();
|
||||
@@ -394,7 +399,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.mix.listener.MixStep;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
public MixStatusCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
setContentDisplay(ContentDisplay.LEFT);
|
||||
setGraphicTextGap(8);
|
||||
getStyleClass().add("mixstatus-cell");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(UtxoEntry.MixStatus mixStatus, boolean empty) {
|
||||
super.updateItem(mixStatus, empty);
|
||||
|
||||
EntryCell.applyRowStyles(this, mixStatus == null ? null : mixStatus.getUtxoEntry());
|
||||
|
||||
if(empty || mixStatus == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(Integer.toString(mixStatus.getMixesDone()));
|
||||
if(mixStatus.getNextMixUtxo() == null) {
|
||||
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
}
|
||||
|
||||
if(mixStatus.getNextMixUtxo() != null) {
|
||||
setMixSuccess(mixStatus.getNextMixUtxo());
|
||||
} else if(mixStatus.getMixFailReason() != null) {
|
||||
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError());
|
||||
} else if(mixStatus.getMixProgress() != null) {
|
||||
setMixProgress(mixStatus.getMixProgress());
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setMixSuccess(Utxo nextMixUtxo) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(-1);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
|
||||
setTooltip(tt);
|
||||
}
|
||||
|
||||
private void setMixFail(MixFailReason mixFailReason, String mixError) {
|
||||
if(mixFailReason != MixFailReason.CANCEL) {
|
||||
setGraphic(getFailGlyph());
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
|
||||
"\nMix failures are generally caused by peers disconnecting during a mix." +
|
||||
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
|
||||
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
|
||||
setTooltip(tt);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPlatformSleepConfig() {
|
||||
Platform platform = Platform.getCurrent();
|
||||
if(platform == Platform.OSX) {
|
||||
return "OSX System Preferences";
|
||||
} else if(platform == Platform.WINDOWS) {
|
||||
return "Windows Control Panel";
|
||||
}
|
||||
|
||||
return "system power settings";
|
||||
}
|
||||
|
||||
private void setMixProgress(MixProgress mixProgress) {
|
||||
if(mixProgress.getMixStep() != MixStep.FAIL) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText(mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1));
|
||||
setTooltip(tt);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private ProgressIndicator getProgressIndicator() {
|
||||
ProgressIndicator progressIndicator;
|
||||
if(getGraphic() instanceof ProgressIndicator) {
|
||||
progressIndicator = (ProgressIndicator)getGraphic();
|
||||
} else {
|
||||
progressIndicator = new ProgressBar();
|
||||
}
|
||||
|
||||
return progressIndicator;
|
||||
}
|
||||
|
||||
private static Glyph getMixGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getStopGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFailGlyph() {
|
||||
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
failGlyph.getStyleClass().add("fail-warning");
|
||||
failGlyph.setFontSize(12);
|
||||
return failGlyph;
|
||||
}
|
||||
|
||||
private static class MixStatusContextMenu extends ContextMenu {
|
||||
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
|
||||
Whirlpool pool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(isMixing) {
|
||||
MenuItem mixStop = new MenuItem("Stop Mixing");
|
||||
if(pool != null) {
|
||||
mixStop.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
mixStop.setGraphic(getStopGlyph());
|
||||
mixStop.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mixStop(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixStop);
|
||||
} else {
|
||||
MenuItem mixNow = new MenuItem("Mix Now");
|
||||
if(pool != null) {
|
||||
mixNow.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
|
||||
mixNow.setGraphic(getMixGlyph());
|
||||
mixNow.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mix(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -37,6 +38,7 @@ import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
protected final Wallet wallet;
|
||||
@@ -163,7 +165,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNext(wordEntries.get(i + 1).getEditor());
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
|
||||
}
|
||||
wordsPane.getChildren().addAll(wordEntries);
|
||||
|
||||
@@ -171,7 +174,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
|
||||
if(!displayWordsOnly) {
|
||||
PassphraseEntry passphraseEntry = new PassphraseEntry();
|
||||
wordEntries.get(wordEntries.size() - 1).setNext(passphraseEntry.getEditor());
|
||||
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
|
||||
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
|
||||
vBox.getChildren().add(passphraseEntry);
|
||||
|
||||
@@ -259,7 +262,13 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
//ignore
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
|
||||
invalidLabel.setText("Unsupported Electrum seed");
|
||||
invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly."));
|
||||
} else {
|
||||
invalidLabel.setText("Invalid checksum");
|
||||
invalidLabel.setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,6 +391,14 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
|
||||
if(!dryrun) {
|
||||
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
|
||||
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
|
||||
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
|
||||
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
|
||||
throw new ImportException("Re-entered passphrase did not match");
|
||||
}
|
||||
}
|
||||
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
return true;
|
||||
@@ -392,7 +409,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Import Error", errorMessage);
|
||||
setError("Import Error", errorMessage + ".");
|
||||
importButton.setDisable(false);
|
||||
return false;
|
||||
}
|
||||
@@ -401,7 +418,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
private static class WordEntry extends HBox {
|
||||
private static List<String> wordList;
|
||||
private final TextField wordField;
|
||||
private Node next;
|
||||
private WordEntry nextEntry;
|
||||
private TextField nextField;
|
||||
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
|
||||
super();
|
||||
@@ -411,7 +429,29 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
Label label = new Label((wordNumber+1) + ".");
|
||||
label.setPrefWidth(22);
|
||||
label.setAlignment(Pos.CENTER_RIGHT);
|
||||
wordField = new TextField();
|
||||
wordField = new TextField() {
|
||||
@Override
|
||||
public void paste() {
|
||||
Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||
if(clipboard.hasString() && clipboard.getString().matches("(?m).+[\\n\\s][\\S\\s]*")) {
|
||||
String[] words = clipboard.getString().split("[\\n\\s]");
|
||||
WordEntry entry = WordEntry.this;
|
||||
for(String word : words) {
|
||||
if(entry.nextField != null) {
|
||||
entry.nextField.requestFocus();
|
||||
}
|
||||
|
||||
entry.wordField.setText(word);
|
||||
entry = entry.nextEntry;
|
||||
if(entry == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.paste();
|
||||
}
|
||||
}
|
||||
};
|
||||
wordField.setMaxWidth(100);
|
||||
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
|
||||
String text = change.getText();
|
||||
@@ -431,8 +471,8 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
|
||||
autoCompletionBinding.setDelay(50);
|
||||
autoCompletionBinding.setOnAutoCompleted(event -> {
|
||||
if (next != null) {
|
||||
next.requestFocus();
|
||||
if(nextField != null) {
|
||||
nextField.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -454,8 +494,12 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
return wordField;
|
||||
}
|
||||
|
||||
public void setNext(Node node) {
|
||||
next = node;
|
||||
public void setNextEntry(WordEntry nextEntry) {
|
||||
this.nextEntry = nextEntry;
|
||||
}
|
||||
|
||||
public void setNextField(TextField field) {
|
||||
this.nextField = field;
|
||||
}
|
||||
|
||||
public static boolean isValid(String word) {
|
||||
|
||||
@@ -33,9 +33,9 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class);
|
||||
|
||||
private static final int MIN_FRAGMENT_LENGTH = 10;
|
||||
private static final int MAX_FRAGMENT_LENGTH = 100;
|
||||
private static final int MAX_FRAGMENT_LENGTH = 250;
|
||||
|
||||
private static final int ANIMATION_PERIOD_MILLIS = 400;
|
||||
private static final int ANIMATION_PERIOD_MILLIS = 200;
|
||||
|
||||
private static final int QR_WIDTH = 480;
|
||||
private static final int QR_HEIGHT = 480;
|
||||
@@ -232,7 +232,7 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
if(useLegacyEncoding) {
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
|
||||
} else {
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.github.sarxos.webcam.WebcamEvent;
|
||||
import com.github.sarxos.webcam.WebcamListener;
|
||||
import com.github.sarxos.webcam.WebcamResolution;
|
||||
import com.github.sarxos.webcam.WebcamUpdater;
|
||||
import com.github.sarxos.webcam.*;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
@@ -41,6 +38,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Borders;
|
||||
import org.slf4j.Logger;
|
||||
@@ -74,6 +72,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
|
||||
|
||||
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
||||
|
||||
public QRScanDialog() {
|
||||
this.decoder = new URDecoder();
|
||||
this.legacyDecoder = new LegacyURDecoder();
|
||||
@@ -82,7 +82,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
webcamResolutionProperty.set(WebcamResolution.HD);
|
||||
}
|
||||
|
||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), new QRScanListener(), new ScanDelayCalculator());
|
||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
|
||||
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
|
||||
webcamService.setRestartOnFailure(false);
|
||||
WebcamView webcamView = new WebcamView(webcamService);
|
||||
@@ -104,6 +104,15 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
if(percentComplete.get() <= 0.0) {
|
||||
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
|
||||
for(WebcamDevice device : WebcamScanDriver.getFoundDevices()) {
|
||||
if(device.getName().equals(Config.get().getWebcamDevice())) {
|
||||
webcamDeviceProperty.set(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
VBox vBox = new VBox(20);
|
||||
@@ -122,6 +131,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS &&
|
||||
nested.getMessage().startsWith("Library 'OpenIMAJGrabber' was not loaded successfully from file")) {
|
||||
exception = new WebcamDependencyException("Your system is missing a dependency required for the webcam. Follow the link below for more details.\n\n[https://sparrowwallet.com/docs/faq.html#your-system-is-missing-a-dependency-for-the-webcam]", exception);
|
||||
} else if(nested.getMessage().startsWith("Cannot start native grabber") && Config.get().getWebcamDevice() != null) {
|
||||
exception = new WebcamOpenException("Cannot open configured webcam " + Config.get().getWebcamDevice() + ", reverting to the default webcam");
|
||||
Config.get().setWebcamDevice(null);
|
||||
}
|
||||
|
||||
final Throwable result = exception;
|
||||
@@ -134,6 +146,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
webcamService.cancel();
|
||||
});
|
||||
webcamDeviceProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Config.get().setWebcamDevice(newValue.getName());
|
||||
if(!Objects.equals(webcamService.getDevice(), newValue)) {
|
||||
webcamService.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
|
||||
@@ -146,7 +164,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().addAll(hdButtonType, cancelButtonType);
|
||||
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2);
|
||||
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
|
||||
dialogPane.setPrefWidth(646);
|
||||
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
@@ -228,7 +247,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
//ignore, bytes not parsable as tx
|
||||
}
|
||||
|
||||
result = new Result(new ScanException("Parsed QR parts were not a PSBT or transaction"));
|
||||
result = new Result(complete);
|
||||
}
|
||||
} else {
|
||||
PSBT psbt;
|
||||
@@ -520,6 +539,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
public void webcamClosed(WebcamEvent webcamEvent) {
|
||||
if(webcamResolutionProperty.get() != null) {
|
||||
webcamService.setResolution(webcamResolutionProperty.get());
|
||||
webcamService.setDevice(webcamDeviceProperty.get());
|
||||
Platform.runLater(() -> {
|
||||
if(!webcamService.isRunning()) {
|
||||
webcamService.reset();
|
||||
@@ -558,10 +578,32 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
});
|
||||
|
||||
button = hd;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
|
||||
ComboBox<WebcamDevice> devicesCombo = new ComboBox<>(WebcamScanDriver.getFoundDevices());
|
||||
devicesCombo.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(WebcamDevice device) {
|
||||
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebcamDevice fromString(String string) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
});
|
||||
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
|
||||
ButtonBar.setButtonData(devicesCombo, ButtonBar.ButtonData.LEFT);
|
||||
|
||||
button = devicesCombo;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
||||
if(button instanceof Region) {
|
||||
((Region)button).setPrefWidth(150);
|
||||
((Region)button).setMaxWidth(150);
|
||||
}
|
||||
|
||||
button.disableProperty().bind(webcamService.openingProperty());
|
||||
return button;
|
||||
}
|
||||
@@ -570,7 +612,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
if(isHd) {
|
||||
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
|
||||
} else {
|
||||
hd.setGraphic(getGlyph(FontAwesome5.Glyph.QUESTION_CIRCLE));
|
||||
hd.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -745,6 +787,24 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
}
|
||||
|
||||
public static class WebcamOpenException extends ScanException {
|
||||
public WebcamOpenException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public WebcamOpenException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public WebcamOpenException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public WebcamOpenException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ScanDelayCalculator implements WebcamUpdater.DelayCalculator {
|
||||
public long calculateDelay(long snapshotDuration, double deviceFps) {
|
||||
return Math.max(SCAN_PERIOD_MILLIS - snapshotDuration, 0L);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import org.controlsfx.dialog.ProgressDialog;
|
||||
|
||||
public class ServiceProgressDialog extends ProgressDialog {
|
||||
public ServiceProgressDialog(String title, String header, String imagePath, Worker<?> worker) {
|
||||
super(worker);
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle(title);
|
||||
setHeaderText(header);
|
||||
|
||||
dialogPane.getStyleClass().remove("progress-dialog");
|
||||
Image image = new Image(imagePath);
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,11 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
this("");
|
||||
}
|
||||
|
||||
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue) {
|
||||
public TextAreaDialog(String defaultValue) {
|
||||
this(defaultValue, true);
|
||||
}
|
||||
|
||||
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue, @NamedArg("editable") boolean editable) {
|
||||
final DialogPane dialogPane = new TextAreaDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
|
||||
@@ -39,6 +43,7 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
this.textArea.setMaxWidth(Double.MAX_VALUE);
|
||||
this.textArea.setWrapText(true);
|
||||
this.textArea.getStyleClass().add("fixed-width");
|
||||
this.textArea.setEditable(editable);
|
||||
hbox.getChildren().add(textArea);
|
||||
HBox.setHgrow(this.textArea, Priority.ALWAYS);
|
||||
|
||||
@@ -49,10 +54,13 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
dialogPane.getStyleClass().add("text-input-dialog");
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
dialogPane.getButtonTypes().add(ButtonType.OK);
|
||||
if(editable) {
|
||||
dialogPane.getButtonTypes().add(ButtonType.CANCEL);
|
||||
|
||||
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(scanButtonType);
|
||||
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(scanButtonType);
|
||||
}
|
||||
|
||||
Platform.runLater(textArea::requestFocus);
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Socket;
|
||||
|
||||
public class TorStatusLabel extends Label {
|
||||
private static final Logger log = LoggerFactory.getLogger(TorStatusLabel.class);
|
||||
|
||||
private final TorConnectionTest torConnectionTest = new TorConnectionTest();
|
||||
|
||||
public TorStatusLabel() {
|
||||
getStyleClass().add("tor-status");
|
||||
setPadding(Platform.getCurrent() == Platform.WINDOWS ? new Insets(0, 0, 1, 3) : new Insets(1, 0, 0, 3));
|
||||
setGraphic(getIcon());
|
||||
update();
|
||||
}
|
||||
|
||||
public void update() {
|
||||
boolean proxyInUse = AppServices.isUsingProxy();
|
||||
boolean internal = AppServices.isTorRunning();
|
||||
|
||||
if(!proxyInUse || internal) {
|
||||
torConnectionTest.cancel();
|
||||
if(internal) {
|
||||
setTooltip(new Tooltip("Internal Tor proxy enabled"));
|
||||
}
|
||||
} else if(!torConnectionTest.isRunning()) {
|
||||
torConnectionTest.setPeriod(Duration.seconds(20.0));
|
||||
torConnectionTest.setBackoffStrategy(null);
|
||||
torConnectionTest.setOnSucceeded(workerStateEvent -> {
|
||||
getStyleClass().remove("failure");
|
||||
setTooltip(new Tooltip("External Tor proxy enabled"));
|
||||
});
|
||||
torConnectionTest.setOnFailed(workerStateEvent -> {
|
||||
if(!getStyleClass().contains("failure")) {
|
||||
getStyleClass().add("failure");
|
||||
}
|
||||
setTooltip(new Tooltip("External Tor proxy error: " + workerStateEvent.getSource().getException().getMessage()));
|
||||
log.warn("Failed to connect to external Tor proxy: " + workerStateEvent.getSource().getException().getMessage());
|
||||
});
|
||||
torConnectionTest.start();
|
||||
}
|
||||
}
|
||||
|
||||
private Node getIcon() {
|
||||
Glyph adjust = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ADJUST);
|
||||
adjust.setFontSize(Platform.getCurrent() == Platform.WINDOWS ? 14 : 15);
|
||||
adjust.setRotate(180);
|
||||
|
||||
Glyph bullseye = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BULLSEYE);
|
||||
bullseye.setFontSize(14);
|
||||
|
||||
Group group = new Group();
|
||||
group.getChildren().addAll(adjust, bullseye);
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private static class TorConnectionTest extends ScheduledService<Void> {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
protected Void call() throws IOException {
|
||||
HostAndPort proxyHostAndPort = HostAndPort.fromString(Config.get().getProxyServer());
|
||||
Socket socket = new Socket(proxyHostAndPort.getHost(), proxyHostAndPort.getPort());
|
||||
socket.close();
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -162,28 +162,28 @@ public class TransactionDiagram extends GridPane {
|
||||
topYaxis.setStartX(width * 0.5);
|
||||
topYaxis.setStartY(getDiagramHeight() * 0.5 - 20.0);
|
||||
topYaxis.setEndX(width * 0.5);
|
||||
topYaxis.setEndY(0);
|
||||
topYaxis.setEndY(10);
|
||||
topYaxis.getStyleClass().add("inputs-type");
|
||||
|
||||
Line topBracket = new Line();
|
||||
topBracket.setStartX(width * 0.5);
|
||||
topBracket.setStartY(0);
|
||||
topBracket.setStartY(10);
|
||||
topBracket.setEndX(width);
|
||||
topBracket.setEndY(0);
|
||||
topBracket.setEndY(10);
|
||||
topBracket.getStyleClass().add("inputs-type");
|
||||
|
||||
Line bottomYaxis = new Line();
|
||||
bottomYaxis.setStartX(width * 0.5);
|
||||
bottomYaxis.setStartY(getDiagramHeight());
|
||||
bottomYaxis.setStartY(getDiagramHeight() - 10);
|
||||
bottomYaxis.setEndX(width * 0.5);
|
||||
bottomYaxis.setEndY(getDiagramHeight() * 0.5 + 20.0);
|
||||
bottomYaxis.getStyleClass().add("inputs-type");
|
||||
|
||||
Line bottomBracket = new Line();
|
||||
bottomBracket.setStartX(width * 0.5);
|
||||
bottomBracket.setStartY(getDiagramHeight());
|
||||
bottomBracket.setStartY(getDiagramHeight() - 10);
|
||||
bottomBracket.setEndX(width);
|
||||
bottomBracket.setEndY(getDiagramHeight());
|
||||
bottomBracket.setEndY(getDiagramHeight() - 10);
|
||||
bottomBracket.getStyleClass().add("inputs-type");
|
||||
|
||||
group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket);
|
||||
@@ -344,7 +344,7 @@ public class TransactionDiagram extends GridPane {
|
||||
group.getChildren().add(yaxisLine);
|
||||
|
||||
double width = 140.0;
|
||||
int numOutputs = displayedPayments.size() + (walletTx.getChangeNode() == null ? 1 : 2);
|
||||
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
||||
for(int i = 1; i <= numOutputs; i++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("output-line");
|
||||
@@ -377,12 +377,13 @@ public class TransactionDiagram extends GridPane {
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
for(Payment payment : displayedPayments) {
|
||||
boolean isConsolidation = walletTx.isConsolidationSend(payment);
|
||||
String recipientDesc = payment instanceof AdditionalPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
|
||||
Label recipientLabel = new Label(recipientDesc, isConsolidation ? getConsolidationGlyph() : getPaymentGlyph());
|
||||
Glyph outputGlyph = getOutputGlyph(payment);
|
||||
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
|
||||
String recipientDesc = labelledPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
|
||||
Label recipientLabel = new Label(recipientDesc, outputGlyph);
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(payment instanceof AdditionalPayment ? "additional-label" : "recipient-label");
|
||||
Tooltip recipientTooltip = new Tooltip((isConsolidation ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString()));
|
||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
Tooltip recipientTooltip = new Tooltip((walletTx.isConsolidationSend(payment) ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString()));
|
||||
recipientTooltip.getStyleClass().add("recipient-label");
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientLabel.setTooltip(recipientTooltip);
|
||||
@@ -390,15 +391,16 @@ public class TransactionDiagram extends GridPane {
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
}
|
||||
|
||||
if(walletTx.getChangeNode() != null) {
|
||||
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
|
||||
WalletNode changeNode = changeEntry.getKey();
|
||||
WalletNode defaultChangeNode = walletTx.getWallet().getFreshNode(KeyPurpose.CHANGE);
|
||||
boolean overGapLimit = (walletTx.getChangeNode().getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
|
||||
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
String changeDesc = walletTx.getChangeAddress().toString().substring(0, 8) + "...";
|
||||
String changeDesc = walletTx.getChangeAddress(changeNode).toString().substring(0, 8) + "...";
|
||||
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() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode.getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
|
||||
changeTooltip.getStyleClass().add("change-label");
|
||||
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
changeLabel.setTooltip(changeTooltip);
|
||||
@@ -467,6 +469,24 @@ public class TransactionDiagram extends GridPane {
|
||||
return spacer;
|
||||
}
|
||||
|
||||
public Glyph getOutputGlyph(Payment payment) {
|
||||
if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
|
||||
return getFakeMixGlyph();
|
||||
} else if(walletTx.isConsolidationSend(payment)) {
|
||||
return getConsolidationGlyph();
|
||||
} else if(walletTx.isPremixSend(payment)) {
|
||||
return getPremixGlyph();
|
||||
} else if(walletTx.isBadbankSend(payment)) {
|
||||
return getBadbankGlyph();
|
||||
} else if(payment.getType().equals(Payment.Type.WHIRLPOOL_FEE)) {
|
||||
return getWhirlpoolFeeGlyph();
|
||||
} else if(payment instanceof AdditionalPayment) {
|
||||
return ((AdditionalPayment)payment).getOutputGlyph(this);
|
||||
}
|
||||
|
||||
return getPaymentGlyph();
|
||||
}
|
||||
|
||||
public static Glyph getExcludeGlyph() {
|
||||
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
|
||||
excludeGlyph.getStyleClass().add("exclude-utxo");
|
||||
@@ -488,6 +508,34 @@ public class TransactionDiagram extends GridPane {
|
||||
return consolidationGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getPremixGlyph() {
|
||||
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
premixGlyph.getStyleClass().add("premix-icon");
|
||||
premixGlyph.setFontSize(12);
|
||||
return premixGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getBadbankGlyph() {
|
||||
Glyph badbankGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BIOHAZARD);
|
||||
badbankGlyph.getStyleClass().add("badbank-icon");
|
||||
badbankGlyph.setFontSize(12);
|
||||
return badbankGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getWhirlpoolFeeGlyph() {
|
||||
Glyph whirlpoolFeeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.HAND_HOLDING_WATER);
|
||||
whirlpoolFeeGlyph.getStyleClass().add("whirlpoolfee-icon");
|
||||
whirlpoolFeeGlyph.setFontSize(12);
|
||||
return whirlpoolFeeGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFakeMixGlyph() {
|
||||
Glyph fakeMixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.THEATER_MASKS);
|
||||
fakeMixGlyph.getStyleClass().add("fakemix-icon");
|
||||
fakeMixGlyph.setFontSize(12);
|
||||
return fakeMixGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getTxoGlyph() {
|
||||
return getChangeGlyph();
|
||||
}
|
||||
@@ -578,6 +626,20 @@ public class TransactionDiagram extends GridPane {
|
||||
this.additionalPayments = additionalPayments;
|
||||
}
|
||||
|
||||
public Glyph getOutputGlyph(TransactionDiagram transactionDiagram) {
|
||||
Glyph glyph = null;
|
||||
for(Payment payment : additionalPayments) {
|
||||
Glyph paymentGlyph = transactionDiagram.getOutputGlyph(payment);
|
||||
if(glyph != null && !paymentGlyph.getStyleClass().equals(glyph.getStyleClass())) {
|
||||
return getPaymentGlyph();
|
||||
}
|
||||
|
||||
glyph = paymentGlyph;
|
||||
}
|
||||
|
||||
return glyph;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.protocol.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.IndexRange;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.Clipboard;
|
||||
@@ -25,6 +26,7 @@ public class TransactionHexArea extends CodeArea {
|
||||
private static final int TRUNCATE_AT = 30000;
|
||||
private static final int SEGMENTS_INTERVAL = 250;
|
||||
|
||||
private String fullHex;
|
||||
private List<TransactionSegment> previousSegmentList = new ArrayList<>();
|
||||
|
||||
public TransactionHexArea() {
|
||||
@@ -37,7 +39,7 @@ public class TransactionHexArea extends CodeArea {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
transaction.bitcoinSerializeToStream(baos);
|
||||
|
||||
String fullHex = Utils.bytesToHex(baos.toByteArray());
|
||||
fullHex = Utils.bytesToHex(baos.toByteArray());
|
||||
String hex = fullHex;
|
||||
if(hex.length() > TRUNCATE_AT) {
|
||||
hex = hex.substring(0, TRUNCATE_AT);
|
||||
@@ -242,6 +244,18 @@ public class TransactionHexArea extends CodeArea {
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy() {
|
||||
IndexRange selection = getSelection();
|
||||
if(fullHex != null && selection.getLength() == getLength()) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(fullHex);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
} else {
|
||||
super.copy();
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionSegment {
|
||||
public TransactionSegment(int start, int length, Integer index, Integer witnessIndex, String style) {
|
||||
this.start = start;
|
||||
|
||||
@@ -74,7 +74,7 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
|
||||
private String getCategoryName(Entry entry) {
|
||||
if(entry.getLabel() != null && !entry.getLabel().isEmpty()) {
|
||||
return entry.getLabel().length() > 15 ? entry.getLabel().substring(0, 15) + "..." : entry.getLabel() + "\n" + ((UtxoEntry)entry).getDescription();
|
||||
return entry.getLabel().length() > 15 ? entry.getLabel().substring(0, 15) + "..." + "\n" + ((UtxoEntry)entry).getDescription() : entry.getLabel() + "\n" + ((UtxoEntry)entry).getDescription();
|
||||
}
|
||||
|
||||
return ((UtxoEntry)entry).getDescription();
|
||||
|
||||
@@ -7,6 +7,7 @@ import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class UtxosTreeTable extends CoinTreeTable {
|
||||
@@ -38,18 +39,25 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
});
|
||||
getColumns().add(outputCol);
|
||||
|
||||
TreeTableColumn<Entry, Entry> addressCol = new TreeTableColumn<>("Address");
|
||||
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
|
||||
});
|
||||
addressCol.setCellFactory(p -> new AddressCell());
|
||||
addressCol.setSortable(true);
|
||||
addressCol.setComparator((o1, o2) -> {
|
||||
UtxoEntry entry1 = (UtxoEntry)o1;
|
||||
UtxoEntry entry2 = (UtxoEntry)o2;
|
||||
return entry1.getAddress().toString().compareTo(entry2.getAddress().toString());
|
||||
});
|
||||
getColumns().add(addressCol);
|
||||
if(rootEntry.getWallet().isWhirlpoolMixWallet()) {
|
||||
TreeTableColumn<Entry, UtxoEntry.MixStatus> mixStatusCol = new TreeTableColumn<>("Mixes");
|
||||
mixStatusCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.MixStatus> param) -> {
|
||||
return ((UtxoEntry)param.getValue().getValue()).mixStatusProperty();
|
||||
});
|
||||
mixStatusCol.setCellFactory(p -> new MixStatusCell());
|
||||
mixStatusCol.setSortable(true);
|
||||
mixStatusCol.setComparator(Comparator.comparingInt(UtxoEntry.MixStatus::getMixesDone));
|
||||
getColumns().add(mixStatusCol);
|
||||
} else {
|
||||
TreeTableColumn<Entry, UtxoEntry.AddressStatus> addressCol = new TreeTableColumn<>("Address");
|
||||
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, UtxoEntry.AddressStatus> param) -> {
|
||||
return ((UtxoEntry)param.getValue().getValue()).addressStatusProperty();
|
||||
});
|
||||
addressCol.setCellFactory(p -> new AddressCell());
|
||||
addressCol.setSortable(true);
|
||||
addressCol.setComparator(Comparator.comparing(o -> o.getAddress().toString()));
|
||||
getColumns().add(addressCol);
|
||||
}
|
||||
|
||||
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
|
||||
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
@@ -93,7 +101,9 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
|
||||
public void updateHistory(List<WalletNode> updatedNodes) {
|
||||
//Utxo entries should have already been updated, so only a resort required
|
||||
sort();
|
||||
if(!getRoot().getChildren().isEmpty()) {
|
||||
sort();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateLabel(Entry entry) {
|
||||
|
||||
@@ -44,7 +44,7 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
exporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
|
||||
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||
}
|
||||
|
||||
@@ -48,13 +48,13 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
importAccordion = new Accordion();
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new SpecterDIY());
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), 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 KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
|
||||
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
|
||||
for(WalletImport importer : walletImporters) {
|
||||
FileWalletImportPane importPane = new FileWalletImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
@@ -95,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, devices.size() == 1);
|
||||
DevicePane devicePane = new DevicePane(new Wallet(), device, devices.size() == 1, null);
|
||||
importAccordion.getPanes().add(0, devicePane);
|
||||
}
|
||||
Platform.runLater(() -> EventManager.get().post(new UsbDeviceEvent(devices)));
|
||||
|
||||
@@ -53,6 +53,10 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
|
||||
final VBox content = new VBox(20);
|
||||
name = (CustomTextField)TextFields.createClearableTextField();
|
||||
name.setText(initialName);
|
||||
name.setTextFormatter(new TextFormatter<>((change) -> {
|
||||
change.setText(change.getText().replaceAll("[\\\\/:*?\"<>|]", "_"));
|
||||
return change;
|
||||
}));
|
||||
content.getChildren().add(name);
|
||||
|
||||
HBox existingBox = new HBox(10);
|
||||
|
||||
@@ -131,10 +131,10 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
}
|
||||
|
||||
public enum PasswordRequirement {
|
||||
LOAD("Please enter the wallet password:", "Unlock"),
|
||||
LOAD("Enter the wallet password:", "Unlock"),
|
||||
UPDATE_NEW("Add a password to the wallet?\nLeave empty for no password:", "No Password"),
|
||||
UPDATE_EMPTY("This wallet has no password.\nAdd a password to the wallet?\nLeave empty for no password:", "No Password"),
|
||||
UPDATE_SET("Please re-enter the wallet password:", "Verify Password"),
|
||||
UPDATE_SET("Re-enter the wallet password:", "Verify Password"),
|
||||
UPDATE_CHANGE("Enter the new wallet password.\nLeave empty for no password:", "No Password");
|
||||
|
||||
private final String description;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
|
||||
public class ChildWalletAddedEvent extends WalletChangedEvent {
|
||||
private final Storage storage;
|
||||
private final Wallet childWallet;
|
||||
|
||||
public ChildWalletAddedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) {
|
||||
super(masterWallet);
|
||||
this.storage = storage;
|
||||
this.childWallet = childWallet;
|
||||
}
|
||||
|
||||
public Storage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
public Wallet getChildWallet() {
|
||||
return childWallet;
|
||||
}
|
||||
|
||||
public String getMasterWalletId() {
|
||||
return storage.getWalletId(getWallet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class MixToConfigChangedEvent {
|
||||
private final Wallet wallet;
|
||||
|
||||
public MixToConfigChangedEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -6,21 +6,25 @@ import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.control.CoinLabel;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class NewWalletTransactionsEvent {
|
||||
private final Wallet wallet;
|
||||
private final List<BlockTransaction> blockTransactions;
|
||||
private final List<TransactionEntry> transactionEntries;
|
||||
private final long totalBlockchainValue;
|
||||
private final long totalMempoolValue;
|
||||
|
||||
public NewWalletTransactionsEvent(Wallet wallet, List<BlockTransaction> blockTransactions, long totalBlockchainValue, long totalMempoolValue) {
|
||||
public NewWalletTransactionsEvent(Wallet wallet, List<TransactionEntry> transactionEntries) {
|
||||
this.wallet = wallet;
|
||||
this.blockTransactions = blockTransactions;
|
||||
this.totalBlockchainValue = totalBlockchainValue;
|
||||
this.totalMempoolValue = totalMempoolValue;
|
||||
this.transactionEntries = transactionEntries;
|
||||
this.totalBlockchainValue = transactionEntries.stream().filter(txEntry -> txEntry.getConfirmations() > 0).mapToLong(Entry::getValue).sum();
|
||||
this.totalMempoolValue = transactionEntries.stream().filter(txEntry ->txEntry.getConfirmations() == 0).mapToLong(Entry::getValue).sum();
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
@@ -28,7 +32,7 @@ public class NewWalletTransactionsEvent {
|
||||
}
|
||||
|
||||
public List<BlockTransaction> getBlockTransactions() {
|
||||
return blockTransactions;
|
||||
return transactionEntries.stream().map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public long getTotalValue() {
|
||||
@@ -55,4 +59,13 @@ public class NewWalletTransactionsEvent {
|
||||
|
||||
return String.format(Locale.ENGLISH, "%,d", value) + " sats";
|
||||
}
|
||||
|
||||
public List<BlockTransaction> getWhirlpoolMixTransactions() {
|
||||
List<BlockTransaction> mixTransactions = new ArrayList<>();
|
||||
if(wallet.isWhirlpoolMixWallet()) {
|
||||
return transactionEntries.stream().filter(txEntry -> txEntry.getValue() == 0).map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return mixTransactions;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -12,6 +13,7 @@ public class SpendUtxoEvent {
|
||||
private final List<Payment> payments;
|
||||
private final Long fee;
|
||||
private final boolean includeSpentMempoolOutputs;
|
||||
private final Pool pool;
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
this(wallet, utxos, null, null, false);
|
||||
@@ -23,6 +25,16 @@ public class SpendUtxoEvent {
|
||||
this.payments = payments;
|
||||
this.fee = fee;
|
||||
this.includeSpentMempoolOutputs = includeSpentMempoolOutputs;
|
||||
this.pool = null;
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, Pool pool) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this.payments = payments;
|
||||
this.fee = fee;
|
||||
this.includeSpentMempoolOutputs = false;
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
@@ -44,4 +56,8 @@ public class SpendUtxoEvent {
|
||||
public boolean isIncludeSpentMempoolOutputs() {
|
||||
return includeSpentMempoolOutputs;
|
||||
}
|
||||
|
||||
public Pool getPool() {
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class StorageEvent extends TimedEvent {
|
||||
private static final Logger log = LoggerFactory.getLogger(StorageEvent.class);
|
||||
|
||||
private static boolean firstRunDone = false;
|
||||
private static final Map<String, Long> eventTime = new HashMap<>();
|
||||
|
||||
@@ -22,8 +26,10 @@ public class StorageEvent extends TimedEvent {
|
||||
eventTime.put(walletId, System.currentTimeMillis());
|
||||
timeMills = keyDerivationPeriod;
|
||||
} else if(action == Action.END) {
|
||||
long start = eventTime.get(walletId);
|
||||
if(firstRunDone) {
|
||||
Long start = eventTime.get(walletId);
|
||||
if(start == null) {
|
||||
log.error("Could not find start event time for wallet id " + walletId);
|
||||
} else if(firstRunDone) {
|
||||
keyDerivationPeriod = (int)(System.currentTimeMillis() - start);
|
||||
Config.get().setKeyDerivationPeriod(keyDerivationPeriod);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletLockEvent {
|
||||
private final Wallet wallet;
|
||||
|
||||
public WalletLockEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletMasterMixConfigChangedEvent extends WalletMixConfigChangedEvent {
|
||||
public WalletMasterMixConfigChangedEvent(Wallet wallet) {
|
||||
super(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletMixConfigChangedEvent extends WalletChangedEvent {
|
||||
public WalletMixConfigChangedEvent(Wallet wallet) {
|
||||
super(wallet);
|
||||
}
|
||||
}
|
||||
@@ -19,8 +19,7 @@ public class WalletNodeHistoryChangedEvent {
|
||||
}
|
||||
|
||||
public WalletNode getWalletNode(Wallet wallet) {
|
||||
List<KeyPurpose> keyPurposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
|
||||
for(KeyPurpose keyPurpose : keyPurposes) {
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
WalletNode changedNode = getWalletNode(wallet, keyPurpose);
|
||||
if(changedNode != null) {
|
||||
return changedNode;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
|
||||
public class WalletOpenedEvent {
|
||||
private final Storage storage;
|
||||
private final Wallet wallet;
|
||||
|
||||
public WalletOpenedEvent(Storage storage, Wallet wallet) {
|
||||
this.storage = storage;
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Storage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.sparrow.WalletTabData;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
|
||||
public class WalletTabSelectedEvent extends TabSelectedEvent {
|
||||
public WalletTabSelectedEvent(Tab tab) {
|
||||
@@ -9,6 +10,8 @@ public class WalletTabSelectedEvent extends TabSelectedEvent {
|
||||
}
|
||||
|
||||
public WalletTabData getWalletTabData() {
|
||||
return (WalletTabData)getTabData();
|
||||
TabPane subTabs = (TabPane)getTab().getContent();
|
||||
Tab subTab = subTabs.getSelectionModel().getSelectedItem();
|
||||
return (WalletTabData)subTab.getUserData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletUnlockEvent {
|
||||
private final Wallet wallet;
|
||||
|
||||
public WalletUnlockEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.UtxoMixData;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class WalletUtxoMixesChangedEvent extends WalletChangedEvent {
|
||||
private final Map<Sha256Hash, UtxoMixData> changedUtxoMixes;
|
||||
private final Map<Sha256Hash, UtxoMixData> removedUtxoMixes;
|
||||
|
||||
public WalletUtxoMixesChangedEvent(Wallet wallet, Map<Sha256Hash, UtxoMixData> changedUtxoMixes, Map<Sha256Hash, UtxoMixData> removedUtxoMixes) {
|
||||
super(wallet);
|
||||
this.changedUtxoMixes = changedUtxoMixes;
|
||||
this.removedUtxoMixes = removedUtxoMixes;
|
||||
}
|
||||
|
||||
public Map<Sha256Hash, UtxoMixData> getChangedUtxoMixes() {
|
||||
return changedUtxoMixes;
|
||||
}
|
||||
|
||||
public Map<Sha256Hash, UtxoMixData> getRemovedUtxoMixes() {
|
||||
return removedUtxoMixes;
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,17 @@ package com.sparrowwallet.sparrow.event;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletUtxoStatusChangedEvent extends WalletChangedEvent {
|
||||
private final BlockTransactionHashIndex utxo;
|
||||
import java.util.List;
|
||||
|
||||
public WalletUtxoStatusChangedEvent(Wallet wallet, BlockTransactionHashIndex utxo) {
|
||||
public class WalletUtxoStatusChangedEvent extends WalletChangedEvent {
|
||||
private final List<BlockTransactionHashIndex> utxos;
|
||||
|
||||
public WalletUtxoStatusChangedEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
super(wallet);
|
||||
this.utxo = utxo;
|
||||
this.utxos = utxos;
|
||||
}
|
||||
|
||||
public BlockTransactionHashIndex getUtxo() {
|
||||
return utxo;
|
||||
public List<BlockTransactionHashIndex> getUtxos() {
|
||||
return utxos;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WhirlpoolMixEvent {
|
||||
private final Wallet wallet;
|
||||
private final BlockTransactionHashIndex utxo;
|
||||
private final MixProgress mixProgress;
|
||||
private final Utxo nextUtxo;
|
||||
private final MixFailReason mixFailReason;
|
||||
private final String mixError;
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixProgress mixProgress) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = mixProgress;
|
||||
this.nextUtxo = null;
|
||||
this.mixFailReason = null;
|
||||
this.mixError = null;
|
||||
}
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = null;
|
||||
this.nextUtxo = nextUtxo;
|
||||
this.mixFailReason = null;
|
||||
this.mixError = null;
|
||||
}
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixFailReason mixFailReason, String mixError) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = null;
|
||||
this.nextUtxo = null;
|
||||
this.mixFailReason = mixFailReason;
|
||||
this.mixError = mixError;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public BlockTransactionHashIndex getUtxo() {
|
||||
return utxo;
|
||||
}
|
||||
|
||||
public MixProgress getMixProgress() {
|
||||
return mixProgress;
|
||||
}
|
||||
|
||||
public Utxo getNextUtxo() {
|
||||
return nextUtxo;
|
||||
}
|
||||
|
||||
public MixFailReason getMixFailReason() {
|
||||
return mixFailReason;
|
||||
}
|
||||
|
||||
public String getMixError() {
|
||||
return mixError;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
|
||||
public class WhirlpoolMixSuccessEvent extends WhirlpoolMixEvent {
|
||||
private final WalletNode walletNode;
|
||||
|
||||
public WhirlpoolMixSuccessEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo, WalletNode walletNode) {
|
||||
super(wallet, utxo, nextUtxo);
|
||||
this.walletNode = walletNode;
|
||||
}
|
||||
|
||||
public WalletNode getWalletNode() {
|
||||
return walletNode;
|
||||
}
|
||||
}
|
||||
@@ -15,12 +15,15 @@ public class FontAwesome5 extends GlyphFont {
|
||||
* The individual glyphs offered by the FontAwesome5 font.
|
||||
*/
|
||||
public static enum Glyph implements INamedCharacter {
|
||||
ADJUST('\uf042'),
|
||||
ARROW_CIRCLE_DOWN('\uf0ab'),
|
||||
ANGLE_DOUBLE_RIGHT('\uf101'),
|
||||
ARROW_DOWN('\uf063'),
|
||||
ARROW_UP('\uf062'),
|
||||
BAN('\uf05e'),
|
||||
BIOHAZARD('\uf780'),
|
||||
BTC('\uf15a'),
|
||||
BULLSEYE('\uf140'),
|
||||
CAMERA('\uf030'),
|
||||
CHECK_CIRCLE('\uf058'),
|
||||
CIRCLE('\uf111'),
|
||||
@@ -32,17 +35,23 @@ public class FontAwesome5 extends GlyphFont {
|
||||
EXTERNAL_LINK_ALT('\uf35d'),
|
||||
ELLIPSIS_H('\uf141'),
|
||||
EYE('\uf06e'),
|
||||
FEATHER_ALT('\uf56b'),
|
||||
FILE_CSV('\uf6dd'),
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||
HAND_HOLDING_WATER('\uf4c1'),
|
||||
HISTORY('\uf1da'),
|
||||
INFO_CIRCLE('\uf05a'),
|
||||
KEY('\uf084'),
|
||||
LAPTOP('\uf109'),
|
||||
LOCK('\uf023'),
|
||||
LOCK_OPEN('\uf3c1'),
|
||||
MINUS_CIRCLE('\uf056'),
|
||||
PEN_FANCY('\uf5ac'),
|
||||
PLUS('\uf067'),
|
||||
PLAY_CIRCLE('\uf144'),
|
||||
PLUS_CIRCLE('\uf055'),
|
||||
STOP_CIRCLE('\uf28d'),
|
||||
QRCODE('\uf029'),
|
||||
QUESTION_CIRCLE('\uf059'),
|
||||
RANDOM('\uf074'),
|
||||
@@ -53,14 +62,17 @@ public class FontAwesome5 extends GlyphFont {
|
||||
SIGN_OUT_ALT('\uf2f5'),
|
||||
SQUARE('\uf0c8'),
|
||||
SNOWFLAKE('\uf2dc'),
|
||||
SORT_NUMERIC_DOWN('\uf162'),
|
||||
SUN('\uf185'),
|
||||
THEATER_MASKS('\uf630'),
|
||||
TIMES_CIRCLE('\uf057'),
|
||||
TOGGLE_OFF('\uf204'),
|
||||
TOGGLE_ON('\uf205'),
|
||||
TOOLS('\uf7d9'),
|
||||
UNDO('\uf0e2'),
|
||||
USER_FRIENDS('\uf500'),
|
||||
WALLET('\uf555');
|
||||
WALLET('\uf555'),
|
||||
WEIGHT('\uf496');
|
||||
|
||||
private final char ch;
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -31,6 +28,15 @@ public class Bip39 implements KeystoreMnemonicImport {
|
||||
DeterministicSeed seed = new DeterministicSeed(mnemonicWords, passphrase, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
|
||||
return Keystore.fromSeed(seed, derivation);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
ElectrumMnemonicCode.INSTANCE.check(mnemonicWords);
|
||||
throw new ImportException(new MnemonicException.MnemonicTypeException(DeterministicSeed.Type.ELECTRUM));
|
||||
} catch(Exception ex) {
|
||||
if(ex instanceof ImportException && ex.getCause() instanceof MnemonicException.MnemonicTypeException) {
|
||||
throw (ImportException)ex;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ImportException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
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);
|
||||
ScriptType scriptType = ScriptType.valueOf(cf.addressType);
|
||||
|
||||
for(ExtPublicKey extKey : cf.extendedPublicKeys) {
|
||||
Keystore keystore = new Keystore(extKey.name);
|
||||
try {
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path));
|
||||
} catch(NumberFormatException e) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, scriptType.getDefaultDerivationPath()));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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.getFullName();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -42,23 +42,27 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COLDCARD);
|
||||
|
||||
if(cck.xpub != null && cck.path != null) {
|
||||
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub);
|
||||
if(header.getDefaultScriptType() != scriptType) {
|
||||
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
|
||||
try {
|
||||
if(cck.xpub != null && cck.path != null) {
|
||||
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(cck.xpub);
|
||||
if(header.getDefaultScriptType() != scriptType) {
|
||||
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
|
||||
}
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
|
||||
} else if(scriptType.equals(ScriptType.P2SH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
|
||||
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
|
||||
} else if(scriptType.equals(ScriptType.P2WSH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
|
||||
} else {
|
||||
throw new ImportException("Correct derivation not found for script type: " + scriptType);
|
||||
}
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
|
||||
} else if(scriptType.equals(ScriptType.P2SH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
|
||||
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
|
||||
} else if(scriptType.equals(ScriptType.P2WSH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
|
||||
} else {
|
||||
} catch(NullPointerException e) {
|
||||
throw new ImportException("Correct derivation not found for script type: " + scriptType);
|
||||
}
|
||||
|
||||
@@ -183,7 +187,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
writer.append("# " + getName() + " setup file (created by Sparrow)\n");
|
||||
writer.append("#\n");
|
||||
writer.append("Name: ").append(wallet.getName()).append("\n");
|
||||
writer.append("Name: ").append(wallet.getFullName()).append("\n");
|
||||
writer.append("Policy: ").append(Integer.toString(wallet.getDefaultPolicy().getNumSignaturesRequired())).append(" of ").append(Integer.toString(wallet.getKeystores().size())).append("\n");
|
||||
if(!multipleDerivations) {
|
||||
writer.append("Derivation: ").append(wallet.getKeystores().get(0).getKeyDerivation().getDerivationPath()).append("\n");
|
||||
|
||||
@@ -6,13 +6,12 @@ import com.sparrowwallet.sparrow.Mode;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||
import org.slf4j.Logger;
|
||||
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;
|
||||
@@ -28,6 +27,7 @@ public class Config {
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
private FeeRatesSource feeRatesSource;
|
||||
private FeeRatesSelection feeRatesSelection;
|
||||
private OptimizationStrategy sendOptimizationStrategy;
|
||||
private Currency fiatCurrency;
|
||||
private ExchangeSource exchangeSource;
|
||||
private boolean loadRecentWallets = true;
|
||||
@@ -42,10 +42,12 @@ public class Config {
|
||||
private boolean showTransactionHex = true;
|
||||
private boolean showLoadingLog = false;
|
||||
private boolean showUtxosChart = true;
|
||||
private boolean preventSleep = false;
|
||||
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;
|
||||
@@ -58,6 +60,8 @@ public class Config {
|
||||
private File electrumServerCert;
|
||||
private boolean useProxy;
|
||||
private String proxyServer;
|
||||
private Double appWidth;
|
||||
private Double appHeight;
|
||||
|
||||
private static Config INSTANCE;
|
||||
|
||||
@@ -137,6 +141,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public OptimizationStrategy getSendOptimizationStrategy() {
|
||||
return sendOptimizationStrategy;
|
||||
}
|
||||
|
||||
public void setSendOptimizationStrategy(OptimizationStrategy sendOptimizationStrategy) {
|
||||
this.sendOptimizationStrategy = sendOptimizationStrategy;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Currency getFiatCurrency() {
|
||||
return fiatCurrency;
|
||||
}
|
||||
@@ -267,6 +280,14 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isPreventSleep() {
|
||||
return preventSleep;
|
||||
}
|
||||
|
||||
public void setPreventSleep(boolean preventSleep) {
|
||||
this.preventSleep = preventSleep;
|
||||
}
|
||||
|
||||
public List<File> getRecentWalletFiles() {
|
||||
return recentWalletFiles;
|
||||
}
|
||||
@@ -307,6 +328,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public String getWebcamDevice() {
|
||||
return webcamDevice;
|
||||
}
|
||||
|
||||
public void setWebcamDevice(String webcamDevice) {
|
||||
this.webcamDevice = webcamDevice;
|
||||
flush();
|
||||
}
|
||||
|
||||
public ServerType getServerType() {
|
||||
return serverType;
|
||||
}
|
||||
@@ -395,24 +425,6 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public Boolean getCoreMultiWallet() {
|
||||
return coreMultiWallet;
|
||||
}
|
||||
|
||||
public void setCoreMultiWallet(Boolean coreMultiWallet) {
|
||||
this.coreMultiWallet = coreMultiWallet;
|
||||
flush();
|
||||
}
|
||||
|
||||
public String getCoreWallet() {
|
||||
return coreWallet;
|
||||
}
|
||||
|
||||
public void setCoreWallet(String coreWallet) {
|
||||
this.coreWallet = coreWallet;
|
||||
flush();
|
||||
}
|
||||
|
||||
public String getElectrumServer() {
|
||||
return electrumServer;
|
||||
}
|
||||
@@ -449,6 +461,24 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public Double getAppWidth() {
|
||||
return appWidth;
|
||||
}
|
||||
|
||||
public void setAppWidth(Double appWidth) {
|
||||
this.appWidth = appWidth;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Double getAppHeight() {
|
||||
return appHeight;
|
||||
}
|
||||
|
||||
public void setAppHeight(Double appHeight) {
|
||||
this.appHeight = appHeight;
|
||||
flush();
|
||||
}
|
||||
|
||||
private synchronized void flush() {
|
||||
Gson gson = getGson();
|
||||
try {
|
||||
|
||||
@@ -119,8 +119,8 @@ public class Hwi {
|
||||
|
||||
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) throws DisplayAddressException {
|
||||
try {
|
||||
if(!Arrays.asList(ScriptType.SINGLE_HASH_TYPES).contains(scriptType)) {
|
||||
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only single hash types supported");
|
||||
if(!Arrays.asList(ScriptType.ADDRESSABLE_TYPES).contains(scriptType)) {
|
||||
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only addressable types supported");
|
||||
}
|
||||
|
||||
String descriptor = outputDescriptor.toString();
|
||||
@@ -254,6 +254,7 @@ public class Hwi {
|
||||
|
||||
File hwiHomeDir = getHwiHomeDir();
|
||||
File hwiVersionDir = new File(hwiHomeDir, HWI_VERSION_DIR);
|
||||
IOUtils.deleteDirectory(hwiVersionDir);
|
||||
if(!hwiVersionDir.exists()) {
|
||||
Files.createDirectories(hwiVersionDir.toPath(), PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
}
|
||||
|
||||
@@ -90,7 +90,9 @@ public class IOUtils {
|
||||
// if it is a subdirectory, we just return the directory name
|
||||
entry = entry.substring(0, checkSubdir);
|
||||
}
|
||||
result.add(entry);
|
||||
if(!entry.isEmpty()) {
|
||||
result.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@ public class JsonPersistence implements Persistence {
|
||||
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()));
|
||||
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, wallet, null);
|
||||
wallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
File backupFile = storage.getTempBackup();
|
||||
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, null);
|
||||
@@ -68,8 +68,8 @@ public class JsonPersistence implements Persistence {
|
||||
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()));
|
||||
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, wallet, encryptionKey);
|
||||
wallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
File backupFile = storage.getTempBackup();
|
||||
Wallet backupWallet = backupFile == null ? null : loadWallet(backupFile, encryptionKey);
|
||||
@@ -77,16 +77,16 @@ public class JsonPersistence implements Persistence {
|
||||
return new WalletBackupAndKey(wallet, backupWallet, encryptionKey, keyDeriver, childWallets);
|
||||
}
|
||||
|
||||
private Map<Storage, WalletBackupAndKey> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
|
||||
private Map<WalletBackupAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, ECKey encryptionKey) throws IOException, StorageException {
|
||||
File[] walletFiles = getChildWalletFiles(storage.getWalletFile(), masterWallet);
|
||||
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
|
||||
Map<WalletBackupAndKey, Storage> childWallets = new TreeMap<>();
|
||||
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()));
|
||||
childWallets.put(new WalletBackupAndKey(childWallet, null, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
@@ -200,6 +200,11 @@ public class JsonPersistence implements Persistence {
|
||||
return "BIE1".getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPersisted(Storage storage, Wallet wallet) {
|
||||
return storage.getWalletFile().exists();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException {
|
||||
return getEncryptionKey(password, null, null);
|
||||
|
||||
@@ -16,6 +16,7 @@ public interface Persistence {
|
||||
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;
|
||||
boolean isPersisted(Storage storage, Wallet wallet);
|
||||
ECKey getEncryptionKey(CharSequence password) throws IOException, StorageException;
|
||||
AsymmetricKeyDeriver getKeyDeriver();
|
||||
void setKeyDeriver(AsymmetricKeyDeriver keyDeriver);
|
||||
|
||||
@@ -10,7 +10,7 @@ public class SeedSigner extends SpecterDIY {
|
||||
|
||||
@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.";
|
||||
return "Import QR created on your SeedSigner by selecting Generate XPUB in the Signing Tools menu. Note that SeedSigner currently only supports multisig wallets with a P2WSH script type.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Map;
|
||||
|
||||
public class Sparrow implements WalletImport, WalletExport {
|
||||
private static final Logger log = LoggerFactory.getLogger(Sparrow.class);
|
||||
@@ -29,12 +30,20 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
try {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
File tempFile = File.createTempFile(wallet.getName(), null);
|
||||
Storage tempStorage = new Storage(PersistenceType.JSON, tempFile);
|
||||
Wallet exportedWallet = !wallet.isMasterWallet() ? wallet.getMasterWallet() : wallet;
|
||||
PersistenceType persistenceType = exportedWallet.getChildWallets().isEmpty() ? PersistenceType.JSON : PersistenceType.DB;
|
||||
Persistence persistence = persistenceType.getInstance();
|
||||
Storage storage = AppServices.get().getOpenWallets().get(exportedWallet);
|
||||
File tempFile = File.createTempFile(exportedWallet.getName(), "." + persistenceType.getExtension());
|
||||
tempFile.delete();
|
||||
Storage tempStorage = new Storage(persistence, tempFile);
|
||||
tempStorage.setKeyDeriver(storage.getKeyDeriver());
|
||||
tempStorage.setEncryptionPubKey(storage.getEncryptionPubKey());
|
||||
tempStorage.saveWallet(wallet);
|
||||
tempStorage.saveWallet(exportedWallet);
|
||||
for(Wallet childWallet : exportedWallet.getChildWallets()) {
|
||||
tempStorage.saveWallet(childWallet);
|
||||
}
|
||||
persistence.close();
|
||||
Files.copy(tempStorage.getWalletFile(), outputStream);
|
||||
outputStream.flush();
|
||||
tempStorage.getWalletFile().delete();
|
||||
@@ -53,7 +62,8 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
try {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
return storage.isEncrypted() ? "" : PersistenceType.JSON.getExtension();
|
||||
Wallet exportedWallet = !wallet.isMasterWallet() ? wallet.getMasterWallet() : wallet;
|
||||
return !exportedWallet.getChildWallets().isEmpty() ? PersistenceType.DB.getExtension() : (storage.isEncrypted() ? "" : PersistenceType.JSON.getExtension());
|
||||
} catch(IOException e) {
|
||||
//ignore
|
||||
}
|
||||
@@ -89,13 +99,21 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
try {
|
||||
tempFile = File.createTempFile("sparrow", null);
|
||||
java.nio.file.Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
storage = new Storage(PersistenceType.JSON, tempFile);
|
||||
PersistenceType persistenceType = Storage.detectPersistenceType(tempFile);
|
||||
persistenceType = (persistenceType == null ? PersistenceType.JSON : persistenceType);
|
||||
File tempTypedFile = new File(tempFile.getParentFile(), tempFile.getName() + "." + persistenceType.getExtension());
|
||||
tempFile.renameTo(tempTypedFile);
|
||||
tempFile = tempTypedFile;
|
||||
storage = new Storage(persistenceType, tempFile);
|
||||
if(!isEncrypted(tempFile)) {
|
||||
wallet = storage.loadUnencryptedWallet().getWallet();
|
||||
} else {
|
||||
WalletBackupAndKey walletBackupAndKey = storage.loadEncryptedWallet(password);
|
||||
wallet = walletBackupAndKey.getWallet();
|
||||
wallet.decrypt(walletBackupAndKey.getKey());
|
||||
for(Map.Entry<WalletBackupAndKey, Storage> entry : walletBackupAndKey.getChildWallets().entrySet()) {
|
||||
entry.getKey().getWallet().decrypt(entry.getKey().getKey());
|
||||
}
|
||||
}
|
||||
|
||||
return wallet;
|
||||
|
||||
@@ -30,7 +30,7 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
|
||||
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
keystore.setLabel(getName());
|
||||
keystore.setWalletModel(WalletModel.SPECTER_DIY);
|
||||
keystore.setWalletModel(getWalletModel());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
|
||||
return keystore;
|
||||
@@ -69,7 +69,7 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
try {
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
writer.append("addwallet ").append(wallet.getName()).append("&").append(OutputDescriptor.getOutputDescriptor(wallet).toString().replace('\'', 'h')).append("\n");
|
||||
writer.append("addwallet ").append(wallet.getFullName()).append("&").append(OutputDescriptor.getOutputDescriptor(wallet).toString().replace('\'', 'h')).append("\n");
|
||||
writer.flush();
|
||||
} catch(Exception e) {
|
||||
log.error("Error exporting " + getName() + " wallet", e);
|
||||
|
||||
@@ -21,7 +21,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
try {
|
||||
SpecterWallet specterWallet = new SpecterWallet();
|
||||
specterWallet.label = wallet.getName();
|
||||
specterWallet.label = wallet.getFullName();
|
||||
specterWallet.blockheight = wallet.getTransactions().values().stream().mapToInt(BlockTransactionHash::getHeight).min().orElse(wallet.getStoredBlockHeight());
|
||||
specterWallet.descriptor = OutputDescriptor.getOutputDescriptor(wallet).toString(true);
|
||||
|
||||
@@ -62,11 +62,11 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
wallet.setName(specterWallet.label);
|
||||
|
||||
if(specterWallet.devices != null && specterWallet.devices.size() == wallet.getKeystores().size()) {
|
||||
boolean uniqueLabels = specterWallet.devices.stream().map(d -> d.label).distinct().count() == specterWallet.devices.size();
|
||||
boolean uniqueLabels = specterWallet.devices.stream().map(SpecterWalletDevice::getLabel).distinct().count() == specterWallet.devices.size();
|
||||
for(int i = 0; i < specterWallet.devices.size(); i++) {
|
||||
SpecterWalletDevice device = specterWallet.devices.get(i);
|
||||
Keystore keystore = wallet.getKeystores().get(i);
|
||||
keystore.setLabel(device.label + (uniqueLabels ? "" : " " + i));
|
||||
keystore.setLabel(device.getLabel() + (uniqueLabels ? "" : " " + i));
|
||||
|
||||
WalletModel walletModel = device.getWalletModel();
|
||||
if(walletModel != null) {
|
||||
@@ -161,5 +161,13 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
if(label == null) {
|
||||
label = "Keystore";
|
||||
}
|
||||
|
||||
return label.length() > Keystore.MAX_LABEL_LENGTH - 3 ? label.substring(0, Keystore.MAX_LABEL_LENGTH - 3) : label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@ public class Storage {
|
||||
}
|
||||
|
||||
public boolean isEncrypted() throws IOException {
|
||||
if(!walletFile.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return persistence.isEncrypted(walletFile);
|
||||
}
|
||||
|
||||
@@ -115,6 +119,10 @@ public class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPersisted(Wallet wallet) {
|
||||
return persistence.isPersisted(this, wallet);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
ClosePersistenceService closePersistenceService = new ClosePersistenceService();
|
||||
closePersistenceService.start();
|
||||
@@ -165,6 +173,20 @@ public class Storage {
|
||||
}
|
||||
|
||||
public void deleteTempBackups() {
|
||||
File[] backups = getBackups(Storage.TEMP_BACKUP_PREFIX);
|
||||
if(backups.length > 0) {
|
||||
try {
|
||||
Date date = BACKUP_DATE_FORMAT.parse(getBackupDate(backups[0].getName()));
|
||||
ProcessHandle.Info processInfo = ProcessHandle.current().info();
|
||||
if(processInfo.startInstant().isPresent() && processInfo.startInstant().get().isAfter(date.toInstant())) {
|
||||
File permanent = new File(backups[0].getParent(), backups[0].getName().substring(Storage.TEMP_BACKUP_PREFIX.length() + 1));
|
||||
backups[0].renameTo(permanent);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error copying temporary to permanent backup", e);
|
||||
}
|
||||
}
|
||||
|
||||
deleteBackups(Storage.TEMP_BACKUP_PREFIX);
|
||||
}
|
||||
|
||||
@@ -338,6 +360,11 @@ public class Storage {
|
||||
return type.getInstance().isEncrypted(walletFile);
|
||||
}
|
||||
}
|
||||
|
||||
PersistenceType detectedType = detectPersistenceType(walletFile);
|
||||
if(detectedType != null) {
|
||||
return detectedType.getInstance().isEncrypted(walletFile);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
//ignore
|
||||
}
|
||||
@@ -345,6 +372,24 @@ public class Storage {
|
||||
return FileType.BINARY.equals(IOUtils.getFileType(walletFile));
|
||||
}
|
||||
|
||||
public static PersistenceType detectPersistenceType(File walletFile) {
|
||||
try(Reader reader = new FileReader(walletFile)) {
|
||||
int firstChar = reader.read();
|
||||
|
||||
if(firstChar == 'U' || firstChar == '{') {
|
||||
return PersistenceType.JSON;
|
||||
}
|
||||
|
||||
if(firstChar == 'H') {
|
||||
return PersistenceType.DB;
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Error detecting persistence type", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static File getWalletsBackupDir() {
|
||||
File walletsBackupDir = new File(getWalletsDir(), WALLETS_BACKUP_DIR);
|
||||
if(!walletsBackupDir.exists()) {
|
||||
@@ -504,10 +549,18 @@ public class Storage {
|
||||
public static class KeyDerivationService extends Service<ECKey> {
|
||||
private final Storage storage;
|
||||
private final SecureString password;
|
||||
private final boolean verifyPassword;
|
||||
|
||||
public KeyDerivationService(Storage storage, SecureString password) {
|
||||
this.storage = storage;
|
||||
this.password = password;
|
||||
this.verifyPassword = false;
|
||||
}
|
||||
|
||||
public KeyDerivationService(Storage storage, SecureString password, boolean verifyPassword) {
|
||||
this.storage = storage;
|
||||
this.password = password;
|
||||
this.verifyPassword = verifyPassword;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -515,7 +568,12 @@ public class Storage {
|
||||
return new Task<>() {
|
||||
protected ECKey call() throws IOException, StorageException {
|
||||
try {
|
||||
return storage.getEncryptionKey(password);
|
||||
ECKey encryptionFullKey = storage.getEncryptionKey(password);
|
||||
if(verifyPassword && !ECKey.fromPublicOnly(encryptionFullKey).equals(storage.getEncryptionPubKey())) {
|
||||
throw new InvalidPasswordException("Derived pubkey does not match stored pubkey");
|
||||
}
|
||||
|
||||
return encryptionFullKey;
|
||||
} finally {
|
||||
password.clear();
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class WalletBackupAndKey {
|
||||
public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
|
||||
private final Wallet wallet;
|
||||
private final Wallet backupWallet;
|
||||
private final ECKey encryptionKey;
|
||||
private final Key key;
|
||||
private final Map<Storage, WalletBackupAndKey> childWallets;
|
||||
private final Map<WalletBackupAndKey, Storage> childWallets;
|
||||
|
||||
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map<Storage, WalletBackupAndKey> childWallets) {
|
||||
public WalletBackupAndKey(Wallet wallet, Wallet backupWallet, ECKey encryptionKey, AsymmetricKeyDeriver keyDeriver, Map<WalletBackupAndKey, Storage> childWallets) {
|
||||
this.wallet = wallet;
|
||||
this.backupWallet = backupWallet;
|
||||
this.encryptionKey = encryptionKey;
|
||||
@@ -36,7 +36,7 @@ public class WalletBackupAndKey {
|
||||
return key;
|
||||
}
|
||||
|
||||
public Map<Storage, WalletBackupAndKey> getChildWallets() {
|
||||
public Map<WalletBackupAndKey, Storage> getChildWallets() {
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
@@ -48,4 +48,9 @@ public class WalletBackupAndKey {
|
||||
key.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(WalletBackupAndKey other) {
|
||||
return wallet.compareTo(other.wallet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,16 +45,20 @@ public interface BlockTransactionDao {
|
||||
Map<Sha256Hash, BlockTransaction> existing = getForTxId(txid.getBytes());
|
||||
|
||||
if(existing.isEmpty() && blkTx.getId() == null) {
|
||||
long id = insertBlockTransaction(txid.getBytes(), blkTx.getHash().getBytes(), blkTx.getHeight(), blkTx.getDate(), blkTx.getFee(), blkTx.getLabel(),
|
||||
long id = insertBlockTransaction(txid.getBytes(), blkTx.getHash().getBytes(), blkTx.getHeight(), blkTx.getDate(), blkTx.getFee(), truncate(blkTx.getLabel()),
|
||||
blkTx.getTransaction() == null ? null : blkTx.getTransaction().bitcoinSerialize(),
|
||||
blkTx.getBlockHash() == null ? null : blkTx.getBlockHash().getBytes(), wallet.getId());
|
||||
blkTx.setId(id);
|
||||
} else {
|
||||
Long existingId = existing.get(txid) != null ? existing.get(txid).getId() : blkTx.getId();
|
||||
updateBlockTransaction(txid.getBytes(), blkTx.getHash().getBytes(), blkTx.getHeight(), blkTx.getDate(), blkTx.getFee(), blkTx.getLabel(),
|
||||
updateBlockTransaction(txid.getBytes(), blkTx.getHash().getBytes(), blkTx.getHeight(), blkTx.getDate(), blkTx.getFee(), truncate(blkTx.getLabel()),
|
||||
blkTx.getTransaction() == null ? null : blkTx.getTransaction().bitcoinSerialize(),
|
||||
blkTx.getBlockHash() == null ? null : blkTx.getBlockHash().getBytes(), wallet.getId(), existingId);
|
||||
blkTx.setId(existingId);
|
||||
}
|
||||
}
|
||||
|
||||
default String truncate(String label) {
|
||||
return (label != null && label.length() > 255 ? label.substring(0, 255) : label);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,22 +88,23 @@ public class DbPersistence implements Persistence {
|
||||
Persistence backupPersistence = PersistenceType.DB.getInstance();
|
||||
backupPersistence.setKeyDeriver(keyDeriver);
|
||||
backupWallet = backupPersistence.loadWallet(new Storage(backupPersistence, backupFile), password, encryptionKey).getWallet();
|
||||
backupPersistence.close();
|
||||
}
|
||||
|
||||
Map<Storage, WalletBackupAndKey> childWallets = loadChildWallets(storage, masterWallet, backupWallet, encryptionKey);
|
||||
masterWallet.setChildWallets(childWallets.values().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
Map<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, masterWallet, backupWallet, encryptionKey);
|
||||
masterWallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
return new WalletBackupAndKey(masterWallet, backupWallet, encryptionKey, keyDeriver, childWallets);
|
||||
}
|
||||
|
||||
private Map<Storage, WalletBackupAndKey> loadChildWallets(Storage storage, Wallet masterWallet, Wallet backupWallet, ECKey encryptionKey) throws StorageException {
|
||||
private Map<WalletBackupAndKey, Storage> loadChildWallets(Storage storage, Wallet masterWallet, Wallet backupWallet, ECKey encryptionKey) throws StorageException {
|
||||
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
|
||||
List<String> schemas = jdbi.withHandle(handle -> {
|
||||
return handle.createQuery("show schemas").mapTo(String.class).list();
|
||||
});
|
||||
|
||||
List<String> childSchemas = schemas.stream().filter(schema -> schema.startsWith(WALLET_SCHEMA_PREFIX) && !schema.equals(MASTER_SCHEMA)).collect(Collectors.toList());
|
||||
Map<Storage, WalletBackupAndKey> childWallets = new LinkedHashMap<>();
|
||||
Map<WalletBackupAndKey, Storage> childWallets = new TreeMap<>();
|
||||
for(String schema : childSchemas) {
|
||||
migrate(storage, schema, encryptionKey);
|
||||
|
||||
@@ -116,7 +117,7 @@ public class DbPersistence implements Persistence {
|
||||
return childWallet;
|
||||
});
|
||||
Wallet backupChildWallet = backupWallet == null ? null : backupWallet.getChildWallets().stream().filter(child -> wallet.getName().equals(child.getName())).findFirst().orElse(null);
|
||||
childWallets.put(storage, new WalletBackupAndKey(wallet, backupChildWallet, encryptionKey, keyDeriver, Collections.emptyMap()));
|
||||
childWallets.put(new WalletBackupAndKey(wallet, backupChildWallet, encryptionKey, keyDeriver, Collections.emptyMap()), storage);
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
@@ -180,10 +181,18 @@ public class DbPersistence implements Persistence {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Updating " + wallet.getName() + " on " + Thread.currentThread().getName());
|
||||
log.debug("Updating " + wallet.getFullName() + " on " + Thread.currentThread().getName());
|
||||
log.debug(dirtyPersistables.toString());
|
||||
|
||||
Jdbi jdbi = getJdbi(storage, password);
|
||||
List<String> schemas = jdbi.withHandle(handle -> {
|
||||
return handle.createQuery("show schemas").mapTo(String.class).list();
|
||||
});
|
||||
if(!schemas.contains(getSchema(wallet))) {
|
||||
log.debug("Not persisting update for missing schema " + getSchema(wallet));
|
||||
return;
|
||||
}
|
||||
|
||||
jdbi.useHandle(handle -> {
|
||||
WalletDao walletDao = handle.attach(WalletDao.class);
|
||||
try {
|
||||
@@ -237,8 +246,20 @@ public class DbPersistence implements Persistence {
|
||||
for(Entry entry : dirtyPersistables.labelEntries) {
|
||||
if(entry instanceof TransactionEntry && ((TransactionEntry)entry).getBlockTransaction().getId() != null) {
|
||||
blockTransactionDao.updateLabel(((TransactionEntry)entry).getBlockTransaction().getId(), entry.getLabel());
|
||||
} else if(entry instanceof NodeEntry && ((NodeEntry)entry).getNode().getId() != null) {
|
||||
walletNodeDao.updateNodeLabel(((NodeEntry)entry).getNode().getId(), entry.getLabel());
|
||||
} else if(entry instanceof NodeEntry) {
|
||||
WalletNode addressNode = ((NodeEntry)entry).getNode();
|
||||
if(addressNode.getId() == null) {
|
||||
WalletNode purposeNode = wallet.getNode(addressNode.getKeyPurpose());
|
||||
if(purposeNode.getId() == null) {
|
||||
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null);
|
||||
purposeNode.setId(purposeNodeId);
|
||||
}
|
||||
|
||||
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId());
|
||||
addressNode.setId(nodeId);
|
||||
}
|
||||
|
||||
walletNodeDao.updateNodeLabel(addressNode.getId(), entry.getLabel());
|
||||
} else if(entry instanceof HashIndexEntry && ((HashIndexEntry)entry).getHashIndex().getId() != null) {
|
||||
walletNodeDao.updateTxoLabel(((HashIndexEntry)entry).getHashIndex().getId(), entry.getLabel());
|
||||
}
|
||||
@@ -252,6 +273,24 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
if(dirtyPersistables.mixConfig) {
|
||||
MixConfigDao mixConfigDao = handle.attach(MixConfigDao.class);
|
||||
mixConfigDao.addOrUpdate(wallet, wallet.getMixConfig());
|
||||
}
|
||||
|
||||
if(!dirtyPersistables.changedUtxoMixes.isEmpty()) {
|
||||
UtxoMixDataDao utxoMixDataDao = handle.attach(UtxoMixDataDao.class);
|
||||
for(Map.Entry<Sha256Hash, UtxoMixData> utxoMixDataEntry : dirtyPersistables.changedUtxoMixes.entrySet()) {
|
||||
utxoMixDataDao.addOrUpdate(wallet, utxoMixDataEntry.getKey(), utxoMixDataEntry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
if(!dirtyPersistables.removedUtxoMixes.isEmpty()) {
|
||||
UtxoMixDataDao utxoMixDataDao = handle.attach(UtxoMixDataDao.class);
|
||||
List<Long> ids = dirtyPersistables.removedUtxoMixes.values().stream().map(Persistable::getId).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
utxoMixDataDao.deleteUtxoMixData(ids);
|
||||
}
|
||||
|
||||
if(!dirtyPersistables.labelKeystores.isEmpty()) {
|
||||
KeystoreDao keystoreDao = handle.attach(KeystoreDao.class);
|
||||
for(Keystore keystore : dirtyPersistables.labelKeystores) {
|
||||
@@ -387,6 +426,11 @@ public class DbPersistence implements Persistence {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isPersisted(Storage storage, Wallet wallet) {
|
||||
return wallet.getId() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECKey getEncryptionKey(CharSequence password) throws IOException {
|
||||
return getEncryptionKey(password, null, null);
|
||||
@@ -610,7 +654,22 @@ public class DbPersistence implements Persistence {
|
||||
@Subscribe
|
||||
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).utxoStatuses.add(event.getUtxo());
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).utxoStatuses.addAll(event.getUtxos());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletMixConfigChanged(WalletMixConfigChangedEvent event) {
|
||||
if(persistsFor(event.getWallet()) && event.getWallet().getMixConfig() != null) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).mixConfig = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletUtxoMixesChanged(WalletUtxoMixesChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).changedUtxoMixes.putAll(event.getChangedUtxoMixes());
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).removedUtxoMixes.putAll(event.getRemovedUtxoMixes());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,6 +693,9 @@ public class DbPersistence implements Persistence {
|
||||
public Integer blockHeight = null;
|
||||
public final List<Entry> labelEntries = new ArrayList<>();
|
||||
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>();
|
||||
public boolean mixConfig;
|
||||
public final Map<Sha256Hash, UtxoMixData> changedUtxoMixes = new HashMap<>();
|
||||
public final Map<Sha256Hash, UtxoMixData> removedUtxoMixes = new HashMap<>();
|
||||
public final List<Keystore> labelKeystores = new ArrayList<>();
|
||||
public final List<Keystore> encryptionKeystores = new ArrayList<>();
|
||||
|
||||
@@ -646,6 +708,9 @@ public class DbPersistence implements Persistence {
|
||||
"\nAddress labels:" + labelEntries.stream().filter(entry -> entry instanceof NodeEntry).map(entry -> ((NodeEntry)entry).getNode().toString() + " " + entry.getLabel()).collect(Collectors.toList()) +
|
||||
"\nUTXO labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) +
|
||||
"\nUTXO statuses:" + utxoStatuses +
|
||||
"\nMix config:" + mixConfig +
|
||||
"\nUTXO mixes changed:" + changedUtxoMixes +
|
||||
"\nUTXO mixes removed:" + removedUtxoMixes +
|
||||
"\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
|
||||
"\nKeystore encryptions:" + encryptionKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ public interface KeystoreDao {
|
||||
}
|
||||
}
|
||||
|
||||
long id = insert(keystore.getLabel(), keystore.getSource().ordinal(), keystore.getWalletModel().ordinal(),
|
||||
long id = insert(truncate(keystore.getLabel()), keystore.getSource().ordinal(), keystore.getWalletModel().ordinal(),
|
||||
keystore.hasPrivateKey() ? null : keystore.getKeyDerivation().getMasterFingerprint(),
|
||||
keystore.getKeyDerivation().getDerivationPath(),
|
||||
keystore.hasPrivateKey() ? null : keystore.getExtendedPublicKey().toString(),
|
||||
@@ -99,4 +99,8 @@ public interface KeystoreDao {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default String truncate(String label) {
|
||||
return (label != null && label.length() > 255 ? label.substring(0, 255) : label);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.MixConfig;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
|
||||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
|
||||
public interface MixConfigDao {
|
||||
@SqlQuery("select id, scode, mixOnStartup, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex from mixConfig where wallet = ?")
|
||||
@RegisterRowMapper(MixConfigMapper.class)
|
||||
MixConfig getForWalletId(Long id);
|
||||
|
||||
@SqlUpdate("insert into mixConfig (scode, mixOnStartup, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex, wallet) values (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insertMixConfig(String scode, Boolean mixOnStartup, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet);
|
||||
|
||||
@SqlUpdate("update mixConfig set scode = ?, mixOnStartup = ?, mixToWalletFile = ?, mixToWalletName = ?, minMixes = ?, receiveIndex = ?, changeIndex = ?, wallet = ? where id = ?")
|
||||
void updateMixConfig(String scode, Boolean mixOnStartup, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet, long id);
|
||||
|
||||
default void addMixConfig(Wallet wallet) {
|
||||
if(wallet.getMixConfig() != null) {
|
||||
addOrUpdate(wallet, wallet.getMixConfig());
|
||||
}
|
||||
}
|
||||
|
||||
default void addOrUpdate(Wallet wallet, MixConfig mixConfig) {
|
||||
String mixToWalletFile = null;
|
||||
if(mixConfig.getMixToWalletFile() != null) {
|
||||
mixToWalletFile = mixConfig.getMixToWalletFile().getAbsolutePath();
|
||||
}
|
||||
|
||||
if(mixConfig.getId() == null) {
|
||||
long id = insertMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId());
|
||||
mixConfig.setId(id);
|
||||
} else {
|
||||
updateMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId(), mixConfig.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.MixConfig;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class MixConfigMapper implements RowMapper<MixConfig> {
|
||||
@Override
|
||||
public MixConfig map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
String scode = rs.getString("scode");
|
||||
|
||||
Boolean mixOnStartup = rs.getBoolean("mixOnStartup");
|
||||
if(rs.wasNull()) {
|
||||
mixOnStartup = null;
|
||||
}
|
||||
|
||||
String mixToWalletFile = rs.getString("mixToWalletFile");
|
||||
String mixToWalletName = rs.getString("mixToWalletName");
|
||||
|
||||
Integer minMixes = rs.getInt("minMixes");
|
||||
if(rs.wasNull()) {
|
||||
minMixes = null;
|
||||
}
|
||||
|
||||
MixConfig mixConfig = new MixConfig(scode, mixOnStartup, mixToWalletFile == null ? null : new File(mixToWalletFile), mixToWalletName, minMixes, rs.getInt("receiveIndex"), rs.getInt("changeIndex"));
|
||||
mixConfig.setId(rs.getLong("id"));
|
||||
return mixConfig;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.UtxoMixData;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
|
||||
import org.jdbi.v3.sqlobject.customizer.BindList;
|
||||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface UtxoMixDataDao {
|
||||
@SqlQuery("select id, hash, mixesDone, expired from utxoMixData where wallet = ? order by id")
|
||||
@RegisterRowMapper(UtxoMixDataMapper.class)
|
||||
Map<Sha256Hash, UtxoMixData> getForWalletId(Long id);
|
||||
|
||||
@SqlQuery("select id, hash, mixesDone, expired from utxoMixData where hash = ?")
|
||||
@RegisterRowMapper(UtxoMixDataMapper.class)
|
||||
Map<Sha256Hash, UtxoMixData> getForHash(byte[] hash);
|
||||
|
||||
@SqlUpdate("insert into utxoMixData (hash, mixesDone, expired, wallet) values (?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insertUtxoMixData(byte[] hash, int mixesDone, Long expired, long wallet);
|
||||
|
||||
@SqlUpdate("update utxoMixData set hash = ?, mixesDone = ?, expired = ?, wallet = ? where id = ?")
|
||||
void updateUtxoMixData(byte[] hash, int mixesDone, Long expired, long wallet, long id);
|
||||
|
||||
@SqlUpdate("delete from utxoMixData where id in (<ids>)")
|
||||
void deleteUtxoMixData(@BindList("ids") List<Long> ids);
|
||||
|
||||
@SqlUpdate("delete from utxoMixData where wallet = ?")
|
||||
void clear(long wallet);
|
||||
|
||||
default void addUtxoMixData(Wallet wallet) {
|
||||
for(Map.Entry<Sha256Hash, UtxoMixData> utxoMixDataEntry : wallet.getUtxoMixes().entrySet()) {
|
||||
utxoMixDataEntry.getValue().setId(null);
|
||||
addOrUpdate(wallet, utxoMixDataEntry.getKey(), utxoMixDataEntry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
default void addOrUpdate(Wallet wallet, Sha256Hash hash, UtxoMixData utxoMixData) {
|
||||
Map<Sha256Hash, UtxoMixData> existing = getForHash(hash.getBytes());
|
||||
|
||||
if(existing.isEmpty() && utxoMixData.getId() == null) {
|
||||
long id = insertUtxoMixData(hash.getBytes(), utxoMixData.getMixesDone(), utxoMixData.getExpired(), wallet.getId());
|
||||
utxoMixData.setId(id);
|
||||
} else {
|
||||
Long existingId = existing.get(hash) != null ? existing.get(hash).getId() : utxoMixData.getId();
|
||||
updateUtxoMixData(hash.getBytes(), utxoMixData.getMixesDone(), utxoMixData.getExpired(), wallet.getId(), existingId);
|
||||
utxoMixData.setId(existingId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.UtxoMixData;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
|
||||
public class UtxoMixDataMapper implements RowMapper<Map.Entry<Sha256Hash, UtxoMixData>> {
|
||||
@Override
|
||||
public Map.Entry<Sha256Hash, UtxoMixData> map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
Sha256Hash hash = Sha256Hash.wrap(rs.getBytes("hash"));
|
||||
|
||||
Long expired = rs.getLong("expired");
|
||||
if(rs.wasNull()) {
|
||||
expired = null;
|
||||
}
|
||||
|
||||
UtxoMixData utxoMixData = new UtxoMixData(rs.getInt("mixesDone"), expired);
|
||||
utxoMixData.setId(rs.getLong("id"));
|
||||
|
||||
return new Map.Entry<>() {
|
||||
@Override
|
||||
public Sha256Hash getKey() {
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UtxoMixData getValue() {
|
||||
return utxoMixData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UtxoMixData setValue(UtxoMixData value) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.UtxoMixData;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import org.jdbi.v3.sqlobject.CreateSqlObject;
|
||||
@@ -29,6 +30,12 @@ public interface WalletDao {
|
||||
@CreateSqlObject
|
||||
BlockTransactionDao createBlockTransactionDao();
|
||||
|
||||
@CreateSqlObject
|
||||
MixConfigDao createMixConfigDao();
|
||||
|
||||
@CreateSqlObject
|
||||
UtxoMixDataDao createUtxoMixDataDao();
|
||||
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id")
|
||||
@RegisterRowMapper(WalletMapper.class)
|
||||
List<Wallet> loadAllWallets();
|
||||
@@ -86,6 +93,11 @@ public interface WalletDao {
|
||||
|
||||
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId()); //.stream().collect(Collectors.toMap(BlockTransaction::getHash, Function.identity(), (existing, replacement) -> existing, LinkedHashMap::new));
|
||||
wallet.updateTransactions(blockTransactions);
|
||||
|
||||
wallet.setMixConfig(createMixConfigDao().getForWalletId(wallet.getId()));
|
||||
|
||||
Map<Sha256Hash, UtxoMixData> utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId());
|
||||
wallet.getUtxoMixes().putAll(utxoMixes);
|
||||
}
|
||||
|
||||
default void addWallet(String schema, Wallet wallet) {
|
||||
@@ -93,14 +105,20 @@ public interface WalletDao {
|
||||
setSchema(schema);
|
||||
createPolicyDao().addPolicy(wallet.getDefaultPolicy());
|
||||
|
||||
long id = insert(wallet.getName(), wallet.getNetwork().ordinal(), wallet.getPolicyType().ordinal(), wallet.getScriptType().ordinal(), wallet.getStoredBlockHeight(), wallet.gapLimit(), wallet.getBirthDate(), wallet.getDefaultPolicy().getId());
|
||||
long id = insert(truncate(wallet.getName()), wallet.getNetwork().ordinal(), wallet.getPolicyType().ordinal(), wallet.getScriptType().ordinal(), wallet.getStoredBlockHeight(), wallet.gapLimit(), wallet.getBirthDate(), wallet.getDefaultPolicy().getId());
|
||||
wallet.setId(id);
|
||||
|
||||
createKeystoreDao().addKeystores(wallet);
|
||||
createWalletNodeDao().addWalletNodes(wallet);
|
||||
createBlockTransactionDao().addBlockTransactions(wallet);
|
||||
createMixConfigDao().addMixConfig(wallet);
|
||||
createUtxoMixDataDao().addUtxoMixData(wallet);
|
||||
} finally {
|
||||
setSchema(DbPersistence.DEFAULT_SCHEMA);
|
||||
}
|
||||
}
|
||||
|
||||
default String truncate(String label) {
|
||||
return (label != null && label.length() > 255 ? label.substring(0, 255) : label);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +58,10 @@ public interface WalletNodeDao {
|
||||
|
||||
default void addWalletNodes(Wallet wallet) {
|
||||
for(WalletNode purposeNode : wallet.getPurposeNodes()) {
|
||||
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null);
|
||||
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), truncate(purposeNode.getLabel()), wallet.getId(), null);
|
||||
purposeNode.setId(purposeNodeId);
|
||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
||||
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNodeId);
|
||||
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), truncate(addressNode.getLabel()), wallet.getId(), purposeNodeId);
|
||||
addressNode.setId(addressNodeId);
|
||||
addTransactionOutputs(addressNode);
|
||||
}
|
||||
@@ -84,22 +84,22 @@ public interface WalletNodeDao {
|
||||
if(txo.isSpent()) {
|
||||
BlockTransactionHashIndex spentBy = txo.getSpentBy();
|
||||
if(spentBy.getId() == null) {
|
||||
spentById = insertBlockTransactionHashIndex(spentBy.getHash().getBytes(), spentBy.getHeight(), spentBy.getDate(), spentBy.getFee(), spentBy.getLabel(), spentBy.getIndex(), spentBy.getValue(),
|
||||
spentById = insertBlockTransactionHashIndex(spentBy.getHash().getBytes(), spentBy.getHeight(), spentBy.getDate(), spentBy.getFee(), truncate(spentBy.getLabel()), spentBy.getIndex(), spentBy.getValue(),
|
||||
spentBy.getStatus() == null ? null : spentBy.getStatus().ordinal(), null, addressNode.getId());
|
||||
spentBy.setId(spentById);
|
||||
} else {
|
||||
updateBlockTransactionHashIndex(spentBy.getHash().getBytes(), spentBy.getHeight(), spentBy.getDate(), spentBy.getFee(), spentBy.getLabel(), spentBy.getIndex(), spentBy.getValue(),
|
||||
updateBlockTransactionHashIndex(spentBy.getHash().getBytes(), spentBy.getHeight(), spentBy.getDate(), spentBy.getFee(), truncate(spentBy.getLabel()), spentBy.getIndex(), spentBy.getValue(),
|
||||
spentBy.getStatus() == null ? null : spentBy.getStatus().ordinal(), null, addressNode.getId(), spentBy.getId());
|
||||
spentById = spentBy.getId();
|
||||
}
|
||||
}
|
||||
|
||||
if(txo.getId() == null) {
|
||||
long txoId = insertBlockTransactionHashIndex(txo.getHash().getBytes(), txo.getHeight(), txo.getDate(), txo.getFee(), txo.getLabel(), txo.getIndex(), txo.getValue(),
|
||||
long txoId = insertBlockTransactionHashIndex(txo.getHash().getBytes(), txo.getHeight(), txo.getDate(), txo.getFee(), truncate(txo.getLabel()), txo.getIndex(), txo.getValue(),
|
||||
txo.getStatus() == null ? null : txo.getStatus().ordinal(), spentById, addressNode.getId());
|
||||
txo.setId(txoId);
|
||||
} else {
|
||||
updateBlockTransactionHashIndex(txo.getHash().getBytes(), txo.getHeight(), txo.getDate(), txo.getFee(), txo.getLabel(), txo.getIndex(), txo.getValue(),
|
||||
updateBlockTransactionHashIndex(txo.getHash().getBytes(), txo.getHeight(), txo.getDate(), txo.getFee(), truncate(txo.getLabel()), txo.getIndex(), txo.getValue(),
|
||||
txo.getStatus() == null ? null : txo.getStatus().ordinal(), spentById, addressNode.getId(), txo.getId());
|
||||
}
|
||||
}
|
||||
@@ -113,4 +113,8 @@ public interface WalletNodeDao {
|
||||
clearSpentHistory(wallet.getId());
|
||||
clearHistory(wallet.getId());
|
||||
}
|
||||
|
||||
default String truncate(String label) {
|
||||
return (label != null && label.length() > 255 ? label.substring(0, 255) : label);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,11 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new SpecterDIY());
|
||||
}
|
||||
|
||||
for(KeystoreImport importer : importers) {
|
||||
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer);;
|
||||
importAccordion.getPanes().add(importPane);
|
||||
for(KeystoreFileImport importer : importers) {
|
||||
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
|
||||
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == importer.getWalletModel()) {
|
||||
importAccordion.getPanes().add(importPane);
|
||||
}
|
||||
}
|
||||
|
||||
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||
|
||||
@@ -13,8 +13,10 @@ public class HwUsbDevicesController extends KeystoreImportDetailController {
|
||||
|
||||
public void initializeView(List<Device> devices) {
|
||||
for(Device device : devices) {
|
||||
DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1);
|
||||
deviceAccordion.getPanes().add(devicePane);
|
||||
DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1, getMasterController().getRequiredDerivation());
|
||||
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == device.getModel()) {
|
||||
deviceAccordion.getPanes().add(devicePane);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-2
@@ -1,7 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.keystoreimport;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import javafx.application.Platform;
|
||||
@@ -10,6 +12,7 @@ import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
@@ -27,6 +30,9 @@ public class KeystoreImportController implements Initializable {
|
||||
@FXML
|
||||
private StackPane importPane;
|
||||
|
||||
private KeyDerivation requiredDerivation;
|
||||
private WalletModel requiredModel;
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
|
||||
@@ -53,11 +59,12 @@ public class KeystoreImportController implements Initializable {
|
||||
});
|
||||
}
|
||||
|
||||
public void selectSource(KeystoreSource keystoreSource) {
|
||||
public void selectSource(KeystoreSource keystoreSource, boolean required) {
|
||||
for(Toggle toggle : importMenu.getToggles()) {
|
||||
if(toggle.getUserData().equals(keystoreSource)) {
|
||||
Platform.runLater(() -> importMenu.selectToggle(toggle));
|
||||
return;
|
||||
} else if(required) {
|
||||
((ToggleButton)toggle).setDisable(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,4 +103,20 @@ public class KeystoreImportController implements Initializable {
|
||||
throw new IllegalStateException("Can't find pane", e);
|
||||
}
|
||||
}
|
||||
|
||||
public KeyDerivation getRequiredDerivation() {
|
||||
return requiredDerivation;
|
||||
}
|
||||
|
||||
public void setRequiredDerivation(KeyDerivation requiredDerivation) {
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
}
|
||||
|
||||
public WalletModel getRequiredModel() {
|
||||
return requiredModel;
|
||||
}
|
||||
|
||||
public void setRequiredModel(WalletModel requiredModel) {
|
||||
this.requiredModel = requiredModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
|
||||
}
|
||||
|
||||
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource) {
|
||||
this(wallet, initialSource, null, null, false);
|
||||
}
|
||||
|
||||
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource, KeyDerivation requiredDerivation, WalletModel requiredModel, boolean restrictSource) {
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
@@ -39,11 +43,16 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
|
||||
dialogPane.setContent(Borders.wrap(ksiLoader.load()).emptyBorder().buildAll());
|
||||
keystoreImportController = ksiLoader.getController();
|
||||
keystoreImportController.initializeView(wallet);
|
||||
keystoreImportController.selectSource(initialSource);
|
||||
keystoreImportController.selectSource(initialSource, restrictSource);
|
||||
keystoreImportController.setRequiredDerivation(requiredDerivation);
|
||||
keystoreImportController.setRequiredModel(requiredModel);
|
||||
|
||||
final ButtonType watchOnlyButtonType = new javafx.scene.control.ButtonType(Network.get().getXpubHeader().getDisplayName() + " / Watch Only Wallet", ButtonBar.ButtonData.LEFT);
|
||||
if(!restrictSource) {
|
||||
dialogPane.getButtonTypes().add(watchOnlyButtonType);
|
||||
}
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(watchOnlyButtonType, cancelButtonType);
|
||||
dialogPane.getButtonTypes().add(cancelButtonType);
|
||||
dialogPane.setPrefWidth(650);
|
||||
dialogPane.setPrefHeight(690);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
@@ -21,7 +21,7 @@ public class SwController extends KeystoreImportDetailController {
|
||||
TitledDescriptionPane importPane = null;
|
||||
|
||||
if(importer instanceof KeystoreFileImport) {
|
||||
importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer);
|
||||
importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer, getMasterController().getRequiredDerivation());
|
||||
} else if(importer instanceof KeystoreMnemonicImport) {
|
||||
importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer);
|
||||
} else if(importer instanceof KeystoreXprvImport) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
@@ -7,6 +8,7 @@ import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
@@ -100,6 +102,7 @@ public class Bwt {
|
||||
bwtConfig.gapLimit = gapLimit;
|
||||
} else {
|
||||
bwtConfig.requireAddresses = false;
|
||||
bwtConfig.bitcoindTimeout = 30;
|
||||
}
|
||||
|
||||
bwtConfig.verbose = log.isDebugEnabled() ? 2 : 0;
|
||||
@@ -112,14 +115,19 @@ public class Bwt {
|
||||
|
||||
Config config = Config.get();
|
||||
bwtConfig.bitcoindUrl = config.getCoreServer();
|
||||
if(config.getCoreAuthType() == CoreAuthType.COOKIE && config.getCoreDataDir() != null) {
|
||||
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(Protocol.isOnionAddress(bwtConfig.bitcoindUrl) && torProxy != null) {
|
||||
bwtConfig.bitcoindProxy = torProxy.toString();
|
||||
}
|
||||
|
||||
if((config.getCoreAuthType() == CoreAuthType.COOKIE || config.getCoreAuth() == null || config.getCoreAuth().length() < 2) && config.getCoreDataDir() != null) {
|
||||
bwtConfig.bitcoindDir = config.getCoreDataDir().getAbsolutePath() + "/";
|
||||
} else {
|
||||
bwtConfig.bitcoindAuth = config.getCoreAuth();
|
||||
}
|
||||
if(config.getCoreMultiWallet() != Boolean.FALSE) {
|
||||
bwtConfig.bitcoindWallet = config.getCoreWallet();
|
||||
}
|
||||
|
||||
bwtConfig.bitcoindWallet = DEFAULT_CORE_WALLET;
|
||||
bwtConfig.createWalletIfMissing = true;
|
||||
|
||||
Gson gson = new Gson();
|
||||
@@ -129,6 +137,12 @@ public class Bwt {
|
||||
NativeBwtDaemon.start(jsonConfig, callback);
|
||||
}
|
||||
|
||||
private HostAndPort getTorProxy() {
|
||||
return AppServices.isTorRunning() ?
|
||||
HostAndPort.fromParts("127.0.0.1", TorService.PROXY_PORT) :
|
||||
(Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer().replace("localhost", "127.0.0.1")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down the BWT daemon
|
||||
*
|
||||
@@ -184,6 +198,12 @@ public class Bwt {
|
||||
@SerializedName("bitcoind_wallet")
|
||||
public String bitcoindWallet;
|
||||
|
||||
@SerializedName("bitcoind_proxy")
|
||||
public String bitcoindProxy;
|
||||
|
||||
@SerializedName("bitcoind_timeout")
|
||||
public Integer bitcoindTimeout;
|
||||
|
||||
@SerializedName("create_wallet_if_missing")
|
||||
public Boolean createWalletIfMissing;
|
||||
|
||||
@@ -283,7 +303,7 @@ public class Bwt {
|
||||
|
||||
@Override
|
||||
public void onHttpReady(String addr) {
|
||||
log.info("http ready at " + addr);
|
||||
log.debug("http ready at " + addr);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.github.arteam.simplejsonrpc.client.Transport;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
@@ -51,6 +50,8 @@ public class ElectrumServer {
|
||||
|
||||
private static Map<String, String> retrievedScriptHashes = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private static Map<Sha256Hash, BlockTransaction> retrievedTransactions = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
private static ElectrumServerRpc electrumServerRpc = new SimpleElectrumServerRpc();
|
||||
|
||||
private static String bwtElectrumServer;
|
||||
@@ -94,12 +95,14 @@ public class ElectrumServer {
|
||||
//If changing server, don't rely on previous transaction history
|
||||
if(previousServerAddress != null && !electrumServer.equals(previousServerAddress)) {
|
||||
retrievedScriptHashes.clear();
|
||||
retrievedTransactions.clear();
|
||||
}
|
||||
previousServerAddress = electrumServer;
|
||||
|
||||
HostAndPort server = protocol.getServerHostAndPort(electrumServer);
|
||||
boolean localNetworkAddress = !protocol.isOnionAddress(server) && IpAddressMatcher.isLocalNetworkAddress(server.getHost());
|
||||
|
||||
if(Config.get().isUseProxy() && proxyServer != null && !proxyServer.isBlank()) {
|
||||
if(!localNetworkAddress && Config.get().isUseProxy() && proxyServer != null && !proxyServer.isBlank()) {
|
||||
HostAndPort proxy = HostAndPort.fromString(proxyServer);
|
||||
if(electrumServerCert != null) {
|
||||
transport = protocol.getTransport(server, electrumServerCert, proxy);
|
||||
@@ -251,7 +254,7 @@ public class ElectrumServer {
|
||||
|
||||
if(!newReferences.isEmpty()) {
|
||||
//Look for additional nodes to fetch history for by considering the inputs and outputs of new transactions found
|
||||
log.debug(wallet.getName() + " found new transactions: " + newReferences);
|
||||
log.debug(wallet.getFullName() + " found new transactions: " + newReferences);
|
||||
Set<WalletNode> additionalNodes = new HashSet<>();
|
||||
Map<String, WalletNode> walletScriptHashes = getAllScriptHashes(wallet);
|
||||
for(BlockTransactionHash reference : newReferences) {
|
||||
@@ -392,7 +395,7 @@ public class ElectrumServer {
|
||||
Map<String, String> pathScriptHashes = new LinkedHashMap<>();
|
||||
for(WalletNode node : nodes) {
|
||||
if(node == null) {
|
||||
log.error("Null node for wallet " + wallet.getName() + " subscribing nodes " + nodes + " startIndex " + startIndex, new Throwable());
|
||||
log.error("Null node for wallet " + wallet.getFullName() + " subscribing nodes " + nodes + " startIndex " + startIndex, new Throwable());
|
||||
}
|
||||
|
||||
if(node != null && node.getIndex() >= startIndex) {
|
||||
@@ -697,6 +700,21 @@ public class ElectrumServer {
|
||||
|
||||
if(!transactionOutputs.equals(node.getTransactionOutputs())) {
|
||||
node.updateTransactionOutputs(transactionOutputs);
|
||||
copyPostmixLabels(wallet, transactionOutputs);
|
||||
}
|
||||
}
|
||||
|
||||
public void copyPostmixLabels(Wallet wallet, Set<BlockTransactionHashIndex> newTransactionOutputs) {
|
||||
if(wallet.getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && wallet.getMasterWallet() != null) {
|
||||
for(BlockTransactionHashIndex newRef : newTransactionOutputs) {
|
||||
BlockTransactionHashIndex prevRef = wallet.getWalletTxos().keySet().stream()
|
||||
.filter(txo -> wallet.getMasterWallet().getUtxoMixData(txo) != null && txo.isSpent() && txo.getSpentBy().getHash().equals(newRef.getHash())).findFirst().orElse(null);
|
||||
if(prevRef != null && wallet.getMasterWallet().getUtxoMixData(newRef) != null) {
|
||||
if(newRef.getLabel() == null && prevRef.getLabel() != null) {
|
||||
newRef.setLabel(prevRef.getLabel());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -763,6 +781,31 @@ public class ElectrumServer {
|
||||
return Transaction.DEFAULT_MIN_RELAY_FEE;
|
||||
}
|
||||
|
||||
public Sha256Hash broadcastTransactionPrivately(Transaction transaction) throws ServerException {
|
||||
//If Tor proxy is configured, try all external broadcast sources in random order before falling back to connected Electrum server
|
||||
if(AppServices.isUsingProxy()) {
|
||||
List<BroadcastSource> broadcastSources = Arrays.stream(BroadcastSource.values()).filter(src -> src.getSupportedNetworks().contains(Network.get())).collect(Collectors.toList());
|
||||
Sha256Hash txid = null;
|
||||
for(int i = 1; !broadcastSources.isEmpty(); i++) {
|
||||
try {
|
||||
BroadcastSource broadcastSource = broadcastSources.remove(new Random().nextInt(broadcastSources.size()));
|
||||
txid = broadcastSource.broadcastTransaction(transaction);
|
||||
if(Network.get() != Network.MAINNET || i >= MINIMUM_BROADCASTS || broadcastSources.isEmpty()) {
|
||||
return txid;
|
||||
}
|
||||
} catch(BroadcastSource.BroadcastException e) {
|
||||
//ignore, already logged
|
||||
}
|
||||
}
|
||||
|
||||
if(txid != null) {
|
||||
return txid;
|
||||
}
|
||||
}
|
||||
|
||||
return broadcastTransaction(transaction);
|
||||
}
|
||||
|
||||
public Sha256Hash broadcastTransaction(Transaction transaction) throws ServerException {
|
||||
byte[] rawtxBytes = transaction.bitcoinSerialize();
|
||||
String rawtxHex = Utils.bytesToHex(rawtxBytes);
|
||||
@@ -800,8 +843,7 @@ public class ElectrumServer {
|
||||
|
||||
public static Map<String, WalletNode> getAllScriptHashes(Wallet wallet) {
|
||||
Map<String, WalletNode> scriptHashes = new HashMap<>();
|
||||
List<KeyPurpose> purposes = List.of(KeyPurpose.RECEIVE, KeyPurpose.CHANGE);
|
||||
for(KeyPurpose keyPurpose : purposes) {
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
for(WalletNode childNode : wallet.getNode(keyPurpose).getChildren()) {
|
||||
scriptHashes.put(getScriptHash(wallet, childNode), childNode);
|
||||
}
|
||||
@@ -849,6 +891,10 @@ public class ElectrumServer {
|
||||
|
||||
if(server.startsWith("electrs/")) {
|
||||
String electrsVersion = server.substring("electrs/".length());
|
||||
int dashIndex = electrsVersion.indexOf('-');
|
||||
if(dashIndex > -1) {
|
||||
electrsVersion = electrsVersion.substring(0, dashIndex);
|
||||
}
|
||||
try {
|
||||
Version version = new Version(electrsVersion);
|
||||
if(version.compareTo(ELECTRS_MIN_BATCHING_VERSION) >= 0) {
|
||||
@@ -1230,8 +1276,30 @@ public class ElectrumServer {
|
||||
protected Task<Map<Sha256Hash, BlockTransaction>> createTask() {
|
||||
return new Task<>() {
|
||||
protected Map<Sha256Hash, BlockTransaction> call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
return electrumServer.getReferencedTransactions(references, scriptHash);
|
||||
Map<Sha256Hash, BlockTransaction> transactionMap = new HashMap<>();
|
||||
for(Sha256Hash ref : references) {
|
||||
if(retrievedTransactions.get(ref) != null) {
|
||||
transactionMap.put(ref, retrievedTransactions.get(ref));
|
||||
}
|
||||
}
|
||||
|
||||
Set<Sha256Hash> fetchReferences = new HashSet<>(references);
|
||||
fetchReferences.removeAll(transactionMap.keySet());
|
||||
|
||||
if(!fetchReferences.isEmpty()) {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
Map<Sha256Hash, BlockTransaction> fetchedTransactions = electrumServer.getReferencedTransactions(fetchReferences, scriptHash);
|
||||
transactionMap.putAll(fetchedTransactions);
|
||||
|
||||
for(Map.Entry<Sha256Hash, BlockTransaction> fetchedEntry : fetchedTransactions.entrySet()) {
|
||||
if(fetchedEntry.getValue() != null && !Sha256Hash.ZERO_HASH.equals(fetchedEntry.getValue().getBlockHash()) &&
|
||||
AppServices.getCurrentBlockHeight() != null && fetchedEntry.getValue().getConfirmations(AppServices.getCurrentBlockHeight()) >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
|
||||
retrievedTransactions.put(fetchedEntry.getKey(), fetchedEntry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return transactionMap;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1327,29 +1395,8 @@ public class ElectrumServer {
|
||||
protected Task<Sha256Hash> createTask() {
|
||||
return new Task<>() {
|
||||
protected Sha256Hash call() throws ServerException {
|
||||
//If Tor proxy is configured, try all external broadcast sources in random order before falling back to connected Electrum server
|
||||
if(AppServices.isUsingProxy()) {
|
||||
List<BroadcastSource> broadcastSources = Arrays.stream(BroadcastSource.values()).filter(src -> src.getSupportedNetworks().contains(Network.get())).collect(Collectors.toList());
|
||||
Sha256Hash txid = null;
|
||||
for(int i = 1; !broadcastSources.isEmpty(); i++) {
|
||||
try {
|
||||
BroadcastSource broadcastSource = broadcastSources.remove(new Random().nextInt(broadcastSources.size()));
|
||||
txid = broadcastSource.broadcastTransaction(transaction);
|
||||
if(Network.get() != Network.MAINNET || i >= MINIMUM_BROADCASTS || broadcastSources.isEmpty()) {
|
||||
return txid;
|
||||
}
|
||||
} catch(BroadcastSource.BroadcastException e) {
|
||||
//ignore, already logged
|
||||
}
|
||||
}
|
||||
|
||||
if(txid != null) {
|
||||
return txid;
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
return electrumServer.broadcastTransaction(transaction);
|
||||
return electrumServer.broadcastTransactionPrivately(transaction);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user