mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 12:06:15 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97f312cb93 | ||
|
|
269fd7f0da | ||
|
|
f0a813d031 | ||
|
|
ceb5d85648 | ||
|
|
6b6b23b51a | ||
|
|
180e76f0f8 | ||
|
|
d3b1c51115 | ||
|
|
72cb696451 | ||
|
|
784fa5e1e8 | ||
|
|
37aa3c9712 | ||
|
|
9520f6d218 | ||
|
|
f4810bb568 | ||
|
|
febd5c33a2 | ||
|
|
4b39316821 | ||
|
|
69544a2dc8 | ||
|
|
2a0412320a | ||
|
|
fffd489bcf | ||
|
|
70a6b259ba | ||
|
|
26c177bd00 | ||
|
|
1497b3d3bb | ||
|
|
230f3c3db7 | ||
|
|
813e0f3ab1 | ||
|
|
237f97852d | ||
|
|
97ef580602 | ||
|
|
2debc07375 | ||
|
|
1b3a35fda7 | ||
|
|
23fd597ca5 | ||
|
|
eb012f638e | ||
|
|
63259a2056 | ||
|
|
bad209ea5b | ||
|
|
776fcb3044 | ||
|
|
63ec856e87 | ||
|
|
ada8ca28e8 | ||
|
|
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 | ||
|
|
90dea201b9 | ||
|
|
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 |
+1
-1
@@ -1,3 +1,3 @@
|
||||
[submodule "drongo"]
|
||||
path = drongo
|
||||
url = git@github.com:sparrowwallet/drongo.git
|
||||
url = ../../sparrowwallet/drongo.git
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
Craig Raw <craigraw@gmail.com> Craig Raw <craig@quirk.biz>
|
||||
Craig Raw <craigraw@gmail.com> craigraw <craigraw@gmail.com>
|
||||
@@ -8,11 +8,23 @@ 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).
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
|
||||
`sudo apt install -y rpm fakeroot binutils`
|
||||
|
||||
|
||||
The Sparrow binaries can be built from source using
|
||||
|
||||
`./gradlew jpackage`
|
||||
|
||||
@@ -20,15 +32,17 @@ 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.
|
||||
|
||||
> A video documention of your built process uploaded to [bitcoinbinary.org](https://bitcoinbinary.org/) is appreciated. Alternatively check the site if you wish to see if someone else already verified the provided binaries.
|
||||
|
||||
## Running
|
||||
|
||||
If you prefer to run Sparrow directly from source, it can be launched with
|
||||
If you prefer to run Sparrow directly from source, it can be launched from within the project directory with
|
||||
|
||||
`./sparrow`
|
||||
|
||||
Java 14 or higher must be installed.
|
||||
Java 16 or higher must be installed.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
+150
-9
@@ -5,7 +5,7 @@ plugins {
|
||||
id 'org.beryx.jlink' version '2.24.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.4.3'
|
||||
def sparrowVersion = '1.5.2'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -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'
|
||||
@@ -64,7 +65,7 @@ dependencies {
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.1')
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.2')
|
||||
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
}
|
||||
@@ -90,6 +91,7 @@ dependencies {
|
||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.19')
|
||||
testImplementation('junit:junit:4.12')
|
||||
}
|
||||
|
||||
@@ -115,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",
|
||||
@@ -132,10 +138,11 @@ 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",
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
||||
}
|
||||
}
|
||||
@@ -152,9 +159,12 @@ jlink {
|
||||
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",
|
||||
@@ -174,13 +184,17 @@ jlink {
|
||||
"--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=logback.classic"]
|
||||
"--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"
|
||||
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
|
||||
}
|
||||
}
|
||||
addExtraDependencies("javafx")
|
||||
@@ -188,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) {
|
||||
@@ -231,6 +245,55 @@ task packageTarDistribution(type: Tar) {
|
||||
}
|
||||
|
||||
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')
|
||||
@@ -247,6 +310,7 @@ extraJavaModuleInfo {
|
||||
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')
|
||||
}
|
||||
@@ -312,6 +376,8 @@ extraJavaModuleInfo {
|
||||
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')
|
||||
}
|
||||
@@ -375,7 +441,7 @@ extraJavaModuleInfo {
|
||||
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
|
||||
exports('kotlin')
|
||||
}
|
||||
module('hummingbird-1.6.1.jar', 'com.sparrowwallet.hummingbird', '1.6.1') {
|
||||
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')
|
||||
@@ -383,6 +449,81 @@ extraJavaModuleInfo {
|
||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||
exports('co.nstant.in.cbor')
|
||||
}
|
||||
module('nightjar-0.2.19.jar', 'com.sparrowwallet.nightjar', '0.2.19') {
|
||||
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')
|
||||
|
||||
+15
-5
@@ -58,7 +58,8 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
|
||||
File originalJar = getInputArtifact().get().getAsFile();
|
||||
String originalJarName = originalJar.getName();
|
||||
|
||||
if (isModule(originalJar)) {
|
||||
//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));
|
||||
@@ -122,7 +123,11 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
|
||||
|
||||
private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
|
||||
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
|
||||
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));
|
||||
@@ -136,9 +141,11 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
|
||||
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
|
||||
JarEntry jarEntry = inputStream.getNextJarEntry();
|
||||
while (jarEntry != null) {
|
||||
outputStream.putNextEntry(jarEntry);
|
||||
outputStream.write(inputStream.readAllBytes());
|
||||
outputStream.closeEntry();
|
||||
if(!jarEntry.getName().equals("module-info.class")) {
|
||||
outputStream.putNextEntry(jarEntry);
|
||||
outputStream.write(inputStream.readAllBytes());
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
jarEntry = inputStream.getNextJarEntry();
|
||||
}
|
||||
}
|
||||
@@ -157,6 +164,9 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
|
||||
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();
|
||||
|
||||
@@ -13,6 +13,7 @@ public class ModuleInfo implements Serializable {
|
||||
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;
|
||||
@@ -31,6 +32,10 @@ public class ModuleInfo implements Serializable {
|
||||
this.requiresTransitive.add(requiresTransitive);
|
||||
}
|
||||
|
||||
public void uses(String uses) {
|
||||
this.uses.add(uses);
|
||||
}
|
||||
|
||||
public String getModuleName() {
|
||||
return moduleName;
|
||||
}
|
||||
@@ -50,4 +55,8 @@ public class ModuleInfo implements Serializable {
|
||||
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.2"
|
||||
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: db081695e8...6c03c4106a
@@ -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.3</string>
|
||||
<string>1.5.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -30,16 +30,23 @@ 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.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
@@ -63,6 +70,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.text.ParseException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -114,31 +122,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;
|
||||
|
||||
@@ -157,6 +178,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);
|
||||
@@ -218,8 +245,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));
|
||||
}
|
||||
@@ -263,16 +290,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());
|
||||
|
||||
@@ -280,9 +317,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);
|
||||
});
|
||||
@@ -362,6 +397,9 @@ public class AppController implements Initializable {
|
||||
controller.setStage(stage);
|
||||
controller.initializeView();
|
||||
setStageIcon(stage);
|
||||
stage.setOnShowing(event -> {
|
||||
AppServices.moveToActiveWindowScreen(stage, 600, 460);
|
||||
});
|
||||
|
||||
return stage;
|
||||
} catch(IOException e) {
|
||||
@@ -421,10 +459,7 @@ public class AppController implements Initializable {
|
||||
|
||||
if(file.exists()) {
|
||||
try {
|
||||
byte[] bytes = new byte[(int)file.length()];
|
||||
FileInputStream stream = new FileInputStream(file);
|
||||
stream.read(bytes);
|
||||
stream.close();
|
||||
byte[] bytes = Files.readAllBytes(file.toPath());
|
||||
String name = file.getName();
|
||||
|
||||
try {
|
||||
@@ -615,13 +650,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -640,11 +698,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -704,6 +765,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);
|
||||
@@ -811,13 +878,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 +901,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.getFullDisplayName(), copyKeystore);
|
||||
Optional<String> optionalPassphrase = passphraseDialog.showAndWait();
|
||||
if(optionalPassphrase.isPresent()) {
|
||||
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
copyKeystore.getSeed().setPassphrase("");
|
||||
@@ -862,6 +934,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()) {
|
||||
@@ -895,6 +979,7 @@ public class AppController implements Initializable {
|
||||
List<WalletImport> walletImporters = List.of(new ColdcardSinglesig(), new ColdcardMultisig(),
|
||||
new Electrum(),
|
||||
new SpecterDesktop(),
|
||||
new Descriptor(),
|
||||
new CoboVaultSinglesig(), new CoboVaultMultisig(),
|
||||
new PassportSinglesig(),
|
||||
new KeystoneSinglesig(), new KeystoneMultisig(),
|
||||
@@ -950,11 +1035,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -975,8 +1062,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);
|
||||
}
|
||||
@@ -994,8 +1089,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 {
|
||||
@@ -1016,11 +1120,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
|
||||
@@ -1035,10 +1137,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
|
||||
@@ -1055,11 +1156,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();
|
||||
@@ -1080,17 +1179,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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1118,7 +1222,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);
|
||||
@@ -1131,10 +1235,93 @@ 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(wallet.getMasterWallet().getLabel() != null ? wallet.getMasterWallet().getLabel() : wallet.getMasterWallet().getAutomaticName());
|
||||
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);
|
||||
String label = wallet.getLabel() != null ? wallet.getLabel() : (wallet.isMasterWallet() ? wallet.getAutomaticName() : wallet.getName());
|
||||
Label subTabLabel = new Label(label);
|
||||
subTabLabel.setPadding(new Insets(0, 3, 0, 3));
|
||||
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));
|
||||
@@ -1145,31 +1332,53 @@ public class AppController implements Initializable {
|
||||
controller.setWalletForm(walletForm);
|
||||
|
||||
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
|
||||
tab.setUserData(tabData);
|
||||
subTab.setUserData(tabData);
|
||||
if(!wallet.isWhirlpoolChildWallet()) {
|
||||
subTab.setContextMenu(getSubTabContextMenu(subTab));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public void openExamples(ActionEvent event) {
|
||||
try {
|
||||
addTransactionTab("p2pkh", null, "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac06241559");
|
||||
addTransactionTab("p2sh", null, "0100000003a5ee1a0fd80dfbc3142df136ab56e082b799c13aa977c048bdf8f61bd158652c000000006b48304502203b0160de302cded63589a88214fe499a25aa1d86a2ea09129945cd632476a12c022100c77727daf0718307e184d55df620510cf96d4b5814ae3258519c0482c1ca82fa0121024f4102c1f1cf662bf99f2b034eb03edd4e6c96793cb9445ff519aab580649120ffffffff0fce901eb7b7551ba5f414735ff93b83a2a57403df11059ec88245fba2aaf1a0000000006a47304402204089adb8a1de1a9e22aa43b94d54f1e54dc9bea745d57df1a633e03dd9ede3c2022037d1e53e911ed7212186028f2e085f70524930e22eb6184af090ba4ab779a5b90121030644cb394bf381dbec91680bdf1be1986ad93cfb35603697353199fb285a119effffffff0fce901eb7b7551ba5f414735ff93b83a2a57403df11059ec88245fba2aaf1a0010000009300493046022100a07b2821f96658c938fa9c68950af0e69f3b2ce5f8258b3a6ad254d4bc73e11e022100e82fab8df3f7e7a28e91b3609f91e8ebf663af3a4dc2fd2abd954301a5da67e701475121022afc20bf379bc96a2f4e9e63ffceb8652b2b6a097f63fbee6ecec2a49a48010e2103a767c7221e9f15f870f1ad9311f5ab937d79fcaeee15bb2c722bca515581b4c052aeffffffff02a3b81b00000000001976a914ea00917f128f569cbdf79da5efcd9001671ab52c88ac80969800000000001976a9143dec0ead289be1afa8da127a7dbdd425a05e25f688ac00000000");
|
||||
addTransactionTab("p2sh-p2wpkh", null, "01000000000101db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477010000001716001479091972186c449eb1ded22b78e40d009bdf0089feffffff02b8b4eb0b000000001976a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac0008af2f000000001976a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac02473044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb012103ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a2687392040000");
|
||||
addTransactionTab("p2sh-p2wsh", null, "01000000000101708256c5896fb3f00ef37601f8e30c5b460dbcd1fca1cd7199f9b56fc4ecd5400000000023220020615ae01ed1bc1ffaad54da31d7805d0bb55b52dfd3941114330368c1bbf69b4cffffffff01603edb0300000000160014bbef244bcad13cffb68b5cef3017c7423675552204004730440220010d2854b86b90b7c33661ca25f9d9f15c24b88c5c4992630f77ff004b998fb802204106fc3ec8481fa98e07b7e78809ac91b6ccaf60bf4d3f729c5a75899bb664a501473044022046d66321c6766abcb1366a793f9bfd0e11e0b080354f18188588961ea76c5ad002207262381a0661d66f5c39825202524c45f29d500c6476176cd910b1691176858701695221026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf32103befa190c0c22e2f53720b1be9476dcf11917da4665c44c9c71c3a2d28a933c352102be46dc245f58085743b1cc37c82f0d63a960efa43b5336534275fc469b49f4ac53ae00000000");
|
||||
addTransactionTab("p2wpkh", null, "01000000000101109d2e41430bfdec7e6dfb02bf78b5827eeb717ef25210ff3203b0db8c76c9260000000000ffffffff01a032eb0500000000160014bbef244bcad13cffb68b5cef3017c742367555220247304402202f7cac3494e521018ae0be4ca18517639ef7c00658d42a9f938b2b344c8454e2022039a54218832fad5d14b331329d9042c51ee6be287e95e49ee5b96fda1f5ce13f0121026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf300000000");
|
||||
addTransactionTab("p2wsh", null, "0100000000010193a2db37b841b2a46f4e9bb63fe9c1012da3ab7fe30b9f9c974242778b5af8980000000000ffffffff01806fb307000000001976a914bbef244bcad13cffb68b5cef3017c7423675552288ac040047304402203cdcaf02a44e37e409646e8a506724e9e1394b890cb52429ea65bac4cc2403f1022024b934297bcd0c21f22cee0e48751c8b184cc3a0d704cae2684e14858550af7d01483045022100feb4e1530c13e72226dc912dcd257df90d81ae22dbddb5a3c2f6d86f81d47c8e022069889ddb76388fa7948aaa018b2480ac36132009bb9cfade82b651e88b4b137a01695221026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf32103befa190c0c22e2f53720b1be9476dcf11917da4665c44c9c71c3a2d28a933c352102be46dc245f58085743b1cc37c82f0d63a960efa43b5336534275fc469b49f4ac53ae00000000");
|
||||
//addTransactionTab("test1", null, "02000000000102ba4dc5a4a14bfaa941b7d115b379b5e15f960635cf694c178b9116763cbd63b11600000017160014fc164cbcac023f5eacfcead2d17d8768c41949affeffffff074d44d2856beb68ba52e8832da60a1682768c2421c2d9a8109ef4e66babd1fd1e000000171600148c3098be6b430859115f5ee99c84c368afecd0481500400002305310000000000017a914ffaf369c2212b178c7a2c21c9ccdd5d126e74c4187327f0300000000001976a914a7cda2e06b102a143ab606937a01d152e300cd3e88ac02473044022006da0ca227f765179219e08a33026b94e7cacff77f87b8cd8eb1b46d6dda11d6022064faa7912924fd23406b6ed3328f1bbbc3760dc51109a49c1b38bf57029d304f012103c6a2fcd030270427d4abe1041c8af929a9e2dbab07b243673453847ab842ee1f024730440220786316a16095105a0af28dccac5cf80f449dea2ea810a9559a89ecb989c2cb3d02205cbd9913d1217ffec144ae4f2bd895f16d778c2ec49ae9c929fdc8bcc2a2b1db0121024d4985241609d072a59be6418d700e87688f6c4d99a51ad68e66078211f076ee38820900");
|
||||
//addTransactionTab("3of3-1s.psbt", null, "70736274ff0100550200000001294c4871c059bb76be81e94b78059ee2e0c9b1b47f38edb6b4e75916062394930000000000feffffff01f82a0000000000001976a914e65b294f890792f2c2725d488567018d660f0cf488ac701c09004f0102aa7ed3044b1635bb800000021bf4bfc48934b7966b39bdebb689525d9b8bfed5c8b16e8c58f9afe4641d6d5f03800b5dbec0355c9f0b5e8227bc903e9d0ff1fe6ced0dcfb6d416541c7412c4331406b57041300000800000008000000080020000804f0102aa7ed3042cd31dee80000002d544b2364010378f8c6cec85f6b7ed83a8203dcdbedb97e2625f431f897b837e0363428de8fcfbfe373c0d9e1e0cc8163d886764bafe71c5822eaa232981356589145f63394f300000800000008000000080020000804f0102aa7ed3049ec7d9f580000002793e04aff18b4e40ebc48bcdc6232c54c69cf7265a38fbd85b35705e34d2d42f03368e79aa2b2b7f736d156905a7a45891df07baa2d0b7f127a537908cb82deed514130a48af300000800000008000000080020000800001012b983a000000000000220020f64748dad1cbad107761aaed5c59f25aba006498d260b440e0a091691350c9aa010569532102f26969eb8d1da34d17d33ff99e2f020cc33b3d11d9798ec14f46b82bc455d3262103171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a221037f3794f3be4c4acc086ac84d6902c025713eabf8890f20f44acf0b34e3c0f0f753ae220602f26969eb8d1da34d17d33ff99e2f020cc33b3d11d9798ec14f46b82bc455d3261c130a48af300000800000008000000080020000800000000000000000220603171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a21c5f63394f300000800000008000000080020000800000000000000000220203171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a24830450221008d27cc4b03bc543726e73b69e7980e7364d6f33f979a5cd9b92fb3d050666bd002204fc81fc9c67baf7c3b77041ed316714a9c117a5bdbb020e8c771ea3bdc342434012206037f3794f3be4c4acc086ac84d6902c025713eabf8890f20f44acf0b34e3c0f0f71c06b570413000008000000080000000800200008000000000000000000000");
|
||||
//addTransactionTab("signer.psbt", null, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000");
|
||||
//addTransactionTab("combiner.psbt", null, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000");
|
||||
addTransactionTab("finalizer.psbt", null, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000");
|
||||
} catch(Exception e) {
|
||||
log.error("Error opening examples", 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private void addTransactionTab(String name, File file, String string) throws ParseException, PSBTParseException, TransactionParseException {
|
||||
@@ -1320,6 +1529,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);
|
||||
@@ -1341,6 +1562,28 @@ public class AppController implements Initializable {
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
private ContextMenu getSubTabContextMenu(Tab subTab) {
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
MenuItem rename = new MenuItem("Rename Account");
|
||||
rename.setOnAction(event -> {
|
||||
Label subTabLabel = (Label)subTab.getGraphic();
|
||||
WalletLabelDialog walletLabelDialog = new WalletLabelDialog(subTabLabel.getText());
|
||||
Optional<String> optLabel = walletLabelDialog.showAndWait();
|
||||
if(optLabel.isPresent()) {
|
||||
String label = optLabel.get();
|
||||
subTabLabel.setText(label);
|
||||
|
||||
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
|
||||
Wallet wallet = walletTabData.getWallet();
|
||||
wallet.setLabel(label);
|
||||
EventManager.get().post(new WalletLabelChangedEvent(wallet));
|
||||
}
|
||||
});
|
||||
|
||||
contextMenu.getItems().add(rename);
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
public void setServerType(ServerType serverType) {
|
||||
if(serverType == ServerType.PUBLIC_ELECTRUM_SERVER && !serverToggle.getStyleClass().contains("public-server")) {
|
||||
serverToggle.getStyleClass().add("public-server");
|
||||
@@ -1369,7 +1612,7 @@ public class AppController implements Initializable {
|
||||
private void serverToggleStartAnimation() {
|
||||
Node thumbArea = serverToggle.lookup(".thumb-area");
|
||||
if(thumbArea != null) {
|
||||
FadeTransition fadeTransition = new FadeTransition(Duration.millis(1000), thumbArea);
|
||||
FadeTransition fadeTransition = new FadeTransition(Duration.millis(600), thumbArea);
|
||||
fadeTransition.setFromValue(1.0);
|
||||
fadeTransition.setToValue(0.4);
|
||||
fadeTransition.setAutoReverse(true);
|
||||
@@ -1389,7 +1632,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) {
|
||||
@@ -1417,7 +1660,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) {
|
||||
@@ -1439,6 +1690,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();
|
||||
@@ -1474,6 +1748,7 @@ public class AppController implements Initializable {
|
||||
} else {
|
||||
saveTransaction.setVisible(false);
|
||||
}
|
||||
lockWallet.setDisable(true);
|
||||
exportWallet.setDisable(true);
|
||||
showLoadingLog.setDisable(true);
|
||||
showUtxosChart.setDisable(true);
|
||||
@@ -1483,6 +1758,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);
|
||||
@@ -1507,11 +1783,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());
|
||||
}
|
||||
}
|
||||
@@ -1520,9 +1794,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 {
|
||||
@@ -1537,9 +1846,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 {
|
||||
@@ -1547,29 +1856,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1667,6 +1986,7 @@ public class AppController implements Initializable {
|
||||
String status = CONNECTION_FAILED_PREFIX + event.getMessage();
|
||||
statusUpdated(new StatusEvent(status));
|
||||
serverToggleStopAnimation();
|
||||
setTorIcon();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1675,6 +1995,7 @@ public class AppController implements Initializable {
|
||||
statusUpdated(new StatusEvent(status));
|
||||
setServerToggleTooltip(event.getBlockHeight());
|
||||
serverToggleStopAnimation();
|
||||
setTorIcon();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1694,7 +2015,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("");
|
||||
}
|
||||
@@ -1716,13 +2038,14 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void walletHistoryStarted(WalletHistoryStartedEvent event) {
|
||||
if(AppServices.isConnected() && getOpenWallets().containsKey(event.getWallet())) {
|
||||
if(event.getWalletNode() == null && event.getWallet().getTransactions().isEmpty()) {
|
||||
if(event.getWalletNodes() == null && event.getWallet().getTransactions().isEmpty()) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1736,8 +2059,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);
|
||||
}
|
||||
}
|
||||
@@ -1798,18 +2122,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
|
||||
@@ -1827,23 +2154,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1923,4 +2256,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;
|
||||
}
|
||||
@@ -556,7 +622,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showErrorDialog(String title, String content, ButtonType... buttons) {
|
||||
return showAlertDialog(title, content, Alert.AlertType.ERROR, buttons);
|
||||
return showAlertDialog(title, content == null ? "See log file (Help menu)" : content, Alert.AlertType.ERROR, buttons);
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {
|
||||
@@ -629,7 +695,9 @@ public class AppServices {
|
||||
|
||||
public static void moveToWindowScreen(Window currentWindow, Window newWindow, double newWindowWidth, double newWindowHeight) {
|
||||
Screen currentScreen = Screen.getScreens().stream().filter(screen -> screen.getVisualBounds().contains(currentWindow.getX(), currentWindow.getY())).findFirst().orElse(null);
|
||||
if(currentScreen != null && !Screen.getPrimary().getVisualBounds().contains(currentWindow.getX(), currentWindow.getY()) && !currentScreen.getVisualBounds().contains(newWindow.getX(), newWindow.getY())) {
|
||||
if(currentScreen != null
|
||||
&& ((!Double.isNaN(newWindow.getX()) && !Double.isNaN(newWindow.getY())) || !Screen.getPrimary().getVisualBounds().contains(currentWindow.getX(), currentWindow.getY()))
|
||||
&& !currentScreen.getVisualBounds().contains(newWindow.getX(), newWindow.getY())) {
|
||||
double x = currentWindow.getX() + (currentWindow.getWidth() / 2) - (newWindowWidth / 2);
|
||||
double y = currentWindow.getY() + (currentWindow.getHeight() / 2.2) - (newWindowHeight / 2);
|
||||
newWindow.setX(x);
|
||||
@@ -773,9 +841,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 +856,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 +920,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 +993,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.3";
|
||||
public static final String APP_VERSION = "1.5.2";
|
||||
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,111 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
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.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
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<List<StandardAccount>> {
|
||||
private final ComboBox<StandardAccount> standardAccountCombo;
|
||||
private boolean discoverAccounts = false;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
||||
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
|
||||
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||
dialogPane.getButtonTypes().add(discoverButtonType);
|
||||
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
||||
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());
|
||||
discoverButton.setOnAction(event -> {
|
||||
discoverAccounts = true;
|
||||
});
|
||||
}
|
||||
|
||||
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 ? List.of(standardAccountCombo.getValue()) : (dialogButton == discoverButtonType ? availableAccounts : null));
|
||||
}
|
||||
|
||||
public boolean isDiscoverAccounts() {
|
||||
return discoverAccounts;
|
||||
}
|
||||
}
|
||||
@@ -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().toString() + (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();
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
|
||||
public class ComboBoxTextField extends CustomTextField {
|
||||
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
||||
|
||||
public ComboBoxTextField() {
|
||||
super();
|
||||
getStyleClass().add("combo-text-field");
|
||||
setupCopyButtonField(super.rightProperty());
|
||||
}
|
||||
|
||||
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
|
||||
Region showComboButton = new Region();
|
||||
showComboButton.getStyleClass().addAll("graphic"); //$NON-NLS-1$
|
||||
StackPane showComboButtonPane = new StackPane(showComboButton);
|
||||
showComboButtonPane.getStyleClass().addAll("combo-button"); //$NON-NLS-1$
|
||||
showComboButtonPane.setCursor(Cursor.DEFAULT);
|
||||
showComboButtonPane.setOnMouseReleased(e -> {
|
||||
if(comboProperty.isNotNull().get()) {
|
||||
comboProperty.get().show();
|
||||
}
|
||||
});
|
||||
|
||||
rightProperty.set(showComboButtonPane);
|
||||
}
|
||||
|
||||
public ComboBox<?> getComboProperty() {
|
||||
return comboProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<ComboBox<?>> walletComboProperty() {
|
||||
return comboProperty;
|
||||
}
|
||||
|
||||
public void setComboProperty(ComboBox<?> comboProperty) {
|
||||
this.comboProperty.set(comboProperty);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoresDiscoveredEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class DeviceKeystoreDiscoverDialog extends DeviceDialog<Map<StandardAccount, Keystore>> {
|
||||
private final Wallet masterWallet;
|
||||
private final List<StandardAccount> availableAccounts;
|
||||
|
||||
public DeviceKeystoreDiscoverDialog(List<String> operationFingerprints, Wallet masterWallet, List<StandardAccount> availableAccounts) {
|
||||
super(operationFingerprints);
|
||||
this.masterWallet = masterWallet;
|
||||
this.availableAccounts = availableAccounts;
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
});
|
||||
setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : Collections.emptyMap());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(masterWallet, availableAccounts, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void keystoresDiscovered(KeystoresDiscoveredEvent event) {
|
||||
setResult(event.getDiscoveredKeystores());
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,14 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -33,8 +35,7 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DevicePane extends TitledDescriptionPane {
|
||||
@@ -46,29 +47,32 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
private final OutputDescriptor outputDescriptor;
|
||||
private final KeyDerivation keyDerivation;
|
||||
private final String message;
|
||||
private final List<StandardAccount> availableAccounts;
|
||||
private final Device device;
|
||||
|
||||
private CustomPasswordField pinField;
|
||||
private Button unlockButton;
|
||||
private Button enterPinButton;
|
||||
private Button setPassphraseButton;
|
||||
private SplitMenuButton importButton;
|
||||
private ButtonBase importButton;
|
||||
private Button signButton;
|
||||
private Button displayAddressButton;
|
||||
private Button signMessageButton;
|
||||
private Button discoverKeystoresButton;
|
||||
|
||||
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
||||
|
||||
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.availableAccounts = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
|
||||
@@ -91,6 +95,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
|
||||
@@ -113,6 +118,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
|
||||
@@ -135,6 +141,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = keyDerivation;
|
||||
this.message = message;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
|
||||
@@ -149,6 +156,29 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
this.availableAccounts = availableAccounts;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
|
||||
createSetPassphraseButton();
|
||||
createDiscoverKeystoresButton();
|
||||
|
||||
initialise(device);
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
|
||||
}
|
||||
|
||||
private void initialise(Device device) {
|
||||
if(device.isNeedsPinSent()) {
|
||||
unlockButton.setDefaultButton(defaultDevice);
|
||||
@@ -199,35 +229,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());
|
||||
@@ -277,6 +311,17 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
private void createDiscoverKeystoresButton() {
|
||||
discoverKeystoresButton = new Button("Discover");
|
||||
discoverKeystoresButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
discoverKeystoresButton.setOnAction(event -> {
|
||||
discoverKeystoresButton.setDisable(true);
|
||||
discoverKeystores();
|
||||
});
|
||||
discoverKeystoresButton.managedProperty().bind(discoverKeystoresButton.visibleProperty());
|
||||
discoverKeystoresButton.setVisible(false);
|
||||
}
|
||||
|
||||
private void unlock(Device device) {
|
||||
if(device.getModel().requiresPinPrompt()) {
|
||||
promptPin();
|
||||
@@ -430,7 +475,9 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
setExpanded(true);
|
||||
} else {
|
||||
showOperationButton();
|
||||
setContent(getTogglePassphraseOn());
|
||||
if(!deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||
setContent(getTogglePassphraseOn());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setError("Incorrect PIN", null);
|
||||
@@ -614,6 +661,63 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
signMessageService.start();
|
||||
}
|
||||
|
||||
private void discoverKeystores() {
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
setError("Could not discover keystores", "Only single signature wallets are supported for keystore discovery");
|
||||
return;
|
||||
}
|
||||
|
||||
String masterFingerprint = wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
|
||||
|
||||
Wallet copyWallet = wallet.copy();
|
||||
Map<StandardAccount, String> accountDerivationPaths = new LinkedHashMap<>();
|
||||
for(StandardAccount availableAccount : availableAccounts) {
|
||||
Wallet availableWallet = copyWallet.addChildWallet(availableAccount);
|
||||
Keystore availableKeystore = availableWallet.getKeystores().get(0);
|
||||
String derivationPath = availableKeystore.getKeyDerivation().getDerivationPath();
|
||||
accountDerivationPaths.put(availableAccount, derivationPath);
|
||||
}
|
||||
|
||||
Map<StandardAccount, Keystore> importedKeystores = new LinkedHashMap<>();
|
||||
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), accountDerivationPaths);
|
||||
getXpubsService.setOnSucceeded(workerStateEvent -> {
|
||||
Map<StandardAccount, String> accountXpubs = getXpubsService.getValue();
|
||||
|
||||
for(Map.Entry<StandardAccount, String> entry : accountXpubs.entrySet()) {
|
||||
try {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, accountDerivationPaths.get(entry.getKey())));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
|
||||
importedKeystores.put(entry.getKey(), keystore);
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve xpub", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallet, importedKeystores);
|
||||
walletDiscoveryService.setOnSucceeded(event -> {
|
||||
importedKeystores.keySet().retainAll(walletDiscoveryService.getValue());
|
||||
EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores));
|
||||
});
|
||||
walletDiscoveryService.setOnFailed(event -> {
|
||||
log.error("Failed to discover accounts", event.getSource().getException());
|
||||
setError("Failed to discover accounts", event.getSource().getException().getMessage());
|
||||
discoverKeystoresButton.setDisable(false);
|
||||
});
|
||||
walletDiscoveryService.start();
|
||||
});
|
||||
getXpubsService.setOnFailed(workerStateEvent -> {
|
||||
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
|
||||
discoverKeystoresButton.setDisable(false);
|
||||
});
|
||||
setDescription("Discovering...");
|
||||
showHideLink.setVisible(false);
|
||||
getXpubsService.start();
|
||||
}
|
||||
|
||||
private void showOperationButton() {
|
||||
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||
if(defaultDevice) {
|
||||
@@ -622,7 +726,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);
|
||||
@@ -635,6 +740,10 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
signMessageButton.setDefaultButton(defaultDevice);
|
||||
signMessageButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.DISCOVER_KEYSTORES)) {
|
||||
discoverKeystoresButton.setDefaultButton(defaultDevice);
|
||||
discoverKeystoresButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,6 +751,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 +761,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 +771,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();
|
||||
@@ -679,6 +791,6 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
public enum DeviceOperation {
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE;
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
setText(address.toString());
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(nodeEntry.getNode().getDerivationPath().replace("m", ".."));
|
||||
tooltip.setText(nodeEntry.getNode().toString());
|
||||
setTooltip(tooltip);
|
||||
getStyleClass().add("address-cell");
|
||||
|
||||
@@ -130,6 +130,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
@@ -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());
|
||||
setTitle("Keystore Passphrase" + (walletName != null ? " for " + walletName : ""));
|
||||
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);
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
customSignButton.setOnAction(event -> {
|
||||
customSignButton.setDisable(true);
|
||||
signMessage();
|
||||
setResult(ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
setResult(ButtonBar.ButtonData.OK_DONE);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -399,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,212 @@
|
||||
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.animation.FadeTransition;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
|
||||
|
||||
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(), mixStatus.getMixErrorTimestamp());
|
||||
} else if(mixStatus.getMixProgress() != null) {
|
||||
setMixProgress(mixStatus.getUtxoEntry(), 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, Long mixErrorTimestamp) {
|
||||
if(mixFailReason != MixFailReason.CANCEL) {
|
||||
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
|
||||
if(elapsed >= ERROR_DISPLAY_MILLIS) {
|
||||
//Old error, don't set again.
|
||||
return;
|
||||
}
|
||||
|
||||
Glyph failGlyph = getFailGlyph();
|
||||
setGraphic(failGlyph);
|
||||
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);
|
||||
|
||||
FadeTransition ft = new FadeTransition(Duration.millis(ERROR_DISPLAY_MILLIS - elapsed), failGlyph);
|
||||
ft.setFromValue(1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS));
|
||||
ft.setToValue(0.0);
|
||||
ft.setOnFinished(event -> {
|
||||
setTooltip(null);
|
||||
});
|
||||
ft.play();
|
||||
} 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(UtxoEntry utxoEntry, 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();
|
||||
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1);
|
||||
tt.setText(status);
|
||||
setTooltip(tt);
|
||||
|
||||
if(mixProgress.getMixStep() == MixStep.REGISTERED_INPUT) {
|
||||
tt.setOnShowing(event -> {
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
Whirlpool.RegisteredInputsService registeredInputsService = new Whirlpool.RegisteredInputsService(whirlpool, mixProgress.getPoolId());
|
||||
registeredInputsService.setOnSucceeded(eventStateHandler -> {
|
||||
if(registeredInputsService.getValue() != null) {
|
||||
tt.setText(status + " (1 of " + registeredInputsService.getValue() + ")");
|
||||
}
|
||||
});
|
||||
registeredInputsService.start();
|
||||
});
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,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;
|
||||
@@ -261,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,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;
|
||||
@@ -394,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;
|
||||
}
|
||||
|
||||
@@ -247,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,16 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
@@ -38,6 +38,7 @@ public class TransactionDiagram extends GridPane {
|
||||
private static final int TOOLTIP_SHOW_DELAY = 50;
|
||||
|
||||
private WalletTransaction walletTx;
|
||||
private final BooleanProperty finalProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
public void update(WalletTransaction walletTx) {
|
||||
setMinHeight(getDiagramHeight());
|
||||
@@ -105,7 +106,7 @@ public class TransactionDiagram extends GridPane {
|
||||
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() {
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos();
|
||||
|
||||
if(getPayjoinURI() != null) {
|
||||
if(getPayjoinURI() != null && !selectedUtxos.containsValue(null)) {
|
||||
selectedUtxos = new LinkedHashMap<>(selectedUtxos);
|
||||
selectedUtxos.put(new PayjoinBlockTransactionHashIndex(), null);
|
||||
}
|
||||
@@ -162,28 +163,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);
|
||||
@@ -221,25 +222,36 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
if(walletNode != null) {
|
||||
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + walletNode.getDerivationPath().replace("m", "..") + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
|
||||
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + (isFinal() ? walletTx.getWallet().getFullDisplayName() : "") + " " + walletNode + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
|
||||
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
||||
label.getStyleClass().add("input-label");
|
||||
}
|
||||
|
||||
label.setGraphic(excludeUtxoButton);
|
||||
label.setContentDisplay(ContentDisplay.LEFT);
|
||||
if(!isFinal()) {
|
||||
label.setGraphic(excludeUtxoButton);
|
||||
label.setContentDisplay(ContentDisplay.LEFT);
|
||||
}
|
||||
} else {
|
||||
if(input instanceof PayjoinBlockTransactionHashIndex) {
|
||||
tooltip.setText("Added once transaction is signed and sent to the payjoin server");
|
||||
} else {
|
||||
AdditionalBlockTransactionHashIndex additionalReference = (AdditionalBlockTransactionHashIndex) input;
|
||||
} else if(input instanceof AdditionalBlockTransactionHashIndex additionalReference) {
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
|
||||
joiner.add(getInputDescription(additionalInput));
|
||||
}
|
||||
tooltip.setText(joiner.toString());
|
||||
} else {
|
||||
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
|
||||
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int)input.getIndex());
|
||||
Address fromAddress = txOutput.getScript().getToAddress();
|
||||
tooltip.setText("Input of " + getSatsValue(txOutput.getValue()) + " sats\n" + input.getHashAsString() + ":" + input.getIndex() + (fromAddress != null ? "\n" + fromAddress : ""));
|
||||
} else {
|
||||
tooltip.setText(input.getHashAsString() + ":" + input.getIndex());
|
||||
}
|
||||
label.getStyleClass().add("input-label");
|
||||
}
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
}
|
||||
@@ -344,7 +356,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");
|
||||
@@ -376,48 +388,66 @@ public class TransactionDiagram extends GridPane {
|
||||
outputsBox.setAlignment(Pos.CENTER_LEFT);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
List<OutputNode> outputNodes = new ArrayList<>();
|
||||
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;
|
||||
payment.setLabel(getOutputLabel(payment));
|
||||
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), 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");
|
||||
Wallet toWallet = getToWallet(payment);
|
||||
WalletNode toNode = walletTx.getWallet() != null ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null;
|
||||
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
||||
+ getSatsValue(payment.getAmount()) + " sats to "
|
||||
+ (payment instanceof AdditionalPayment ? "\n" + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : "external address") : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString()));
|
||||
recipientTooltip.getStyleClass().add("recipient-label");
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientLabel.setTooltip(recipientTooltip);
|
||||
outputsBox.getChildren().add(recipientLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
outputNodes.add(new OutputNode(recipientLabel, payment.getAddress()));
|
||||
}
|
||||
|
||||
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) + "...";
|
||||
Address changeAddress = walletTx.getChangeAddress(changeNode);
|
||||
String changeDesc = changeAddress.toString().substring(0, 8) + "...";
|
||||
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
|
||||
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
||||
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 + "\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);
|
||||
actionBox.getChildren().add(changeLabel);
|
||||
|
||||
Button nextChangeAddressButton = new Button("");
|
||||
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
|
||||
nextChangeAddressButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
|
||||
});
|
||||
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
|
||||
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
|
||||
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
|
||||
replaceChangeLabel.getStyleClass().add("replace-change-label");
|
||||
replaceChangeLabel.setVisible(false);
|
||||
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
|
||||
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
|
||||
if(!isFinal()) {
|
||||
Button nextChangeAddressButton = new Button("");
|
||||
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
|
||||
nextChangeAddressButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
|
||||
});
|
||||
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
|
||||
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
|
||||
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
|
||||
replaceChangeLabel.getStyleClass().add("replace-change-label");
|
||||
replaceChangeLabel.setVisible(false);
|
||||
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
|
||||
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
|
||||
actionBox.getChildren().add(replaceChangeLabel);
|
||||
}
|
||||
|
||||
actionBox.getChildren().addAll(changeLabel, replaceChangeLabel);
|
||||
outputsBox.getChildren().add(actionBox);
|
||||
outputNodes.add(new OutputNode(actionBox, changeAddress));
|
||||
}
|
||||
|
||||
if(isFinal()) {
|
||||
Collections.sort(outputNodes);
|
||||
}
|
||||
|
||||
for(OutputNode outputNode : outputNodes) {
|
||||
outputsBox.getChildren().add(outputNode.outputLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
}
|
||||
|
||||
@@ -425,7 +455,7 @@ public class TransactionDiagram extends GridPane {
|
||||
Label feeLabel = highFee ? new Label("High Fee", getWarningGlyph()) : new Label("Fee", getFeeGlyph());
|
||||
feeLabel.getStyleClass().addAll("output-label", "fee-label");
|
||||
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
||||
Tooltip feeTooltip = new Tooltip("Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
||||
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
||||
feeTooltip.getStyleClass().add("fee-tooltip");
|
||||
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
feeLabel.setTooltip(feeTooltip);
|
||||
@@ -443,7 +473,10 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
String txDesc = "Transaction";
|
||||
Label txLabel = new Label(txDesc);
|
||||
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes");
|
||||
boolean isFinalized = walletTx.getTransaction().hasScriptSigs() || walletTx.getTransaction().hasWitnesses();
|
||||
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n"
|
||||
+ String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes"
|
||||
+ (walletTx.getFee() < 0 ? "" : "\n" + String.format("%.2f", walletTx.getFee() / walletTx.getTransaction().getVirtualSize()) + " sats/vB" + (isFinalized ? "" : " (non-final)")));
|
||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
tooltip.getStyleClass().add("transaction-tooltip");
|
||||
txLabel.setTooltip(tooltip);
|
||||
@@ -467,6 +500,57 @@ public class TransactionDiagram extends GridPane {
|
||||
return spacer;
|
||||
}
|
||||
|
||||
private String getOutputLabel(Payment payment) {
|
||||
if(payment.getLabel() != null) {
|
||||
return payment.getLabel();
|
||||
}
|
||||
|
||||
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
|
||||
return "Whirlpool Fee";
|
||||
} else if(walletTx.isPremixSend(payment)) {
|
||||
int premixIndex = getOutputIndex(payment.getAddress()) - 2;
|
||||
return "Premix #" + premixIndex;
|
||||
} else if(walletTx.isBadbankSend(payment)) {
|
||||
return "Badbank Change";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private int getOutputIndex(Address address) {
|
||||
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress())).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private Wallet getToWallet(Payment payment) {
|
||||
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
|
||||
if(openWallet != walletTx.getWallet() && openWallet.isValid() && openWallet.isWalletAddress(payment.getAddress())) {
|
||||
return openWallet;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
} else if(getToWallet(payment) != null) {
|
||||
return getDepositGlyph();
|
||||
}
|
||||
|
||||
return getPaymentGlyph();
|
||||
}
|
||||
|
||||
public static Glyph getExcludeGlyph() {
|
||||
Glyph excludeGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.TIMES_CIRCLE);
|
||||
excludeGlyph.getStyleClass().add("exclude-utxo");
|
||||
@@ -488,6 +572,41 @@ public class TransactionDiagram extends GridPane {
|
||||
return consolidationGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getDepositGlyph() {
|
||||
Glyph depositGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||
depositGlyph.getStyleClass().add("deposit-icon");
|
||||
depositGlyph.setFontSize(12);
|
||||
return depositGlyph;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -541,6 +660,18 @@ public class TransactionDiagram extends GridPane {
|
||||
return lockGlyph;
|
||||
}
|
||||
|
||||
public boolean isFinal() {
|
||||
return finalProperty.get();
|
||||
}
|
||||
|
||||
public BooleanProperty finalProperty() {
|
||||
return finalProperty;
|
||||
}
|
||||
|
||||
public void setFinal(boolean isFinal) {
|
||||
this.finalProperty.set(isFinal);
|
||||
}
|
||||
|
||||
private static class PayjoinBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
public PayjoinBlockTransactionHashIndex() {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
@@ -578,8 +709,41 @@ 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"));
|
||||
}
|
||||
}
|
||||
|
||||
private class OutputNode implements Comparable<OutputNode> {
|
||||
public Node outputLabel;
|
||||
public Address address;
|
||||
|
||||
public OutputNode(Node outputLabel, Address address) {
|
||||
this.outputLabel = outputLabel;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(TransactionDiagram.OutputNode o) {
|
||||
try {
|
||||
return getOutputIndex(address) - getOutputIndex(o.address);
|
||||
} catch(Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -42,9 +42,9 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
|
||||
List<WalletExport> exporters;
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
|
||||
exporters = List.of(new Electrum(), new Descriptor(), new SpecterDesktop(), new Sparrow());
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
exporters = List.of(new CaravanMultisig(), 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 Descriptor(), 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 CaravanMultisig(), 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 Descriptor(), 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)));
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
public class WalletLabelDialog extends Dialog<String> {
|
||||
private final CustomTextField label;
|
||||
|
||||
public WalletLabelDialog(String initialName) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle("Account Name");
|
||||
dialogPane.setHeaderText("Enter a name for this account:");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(400);
|
||||
dialogPane.setPrefHeight(200);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
|
||||
wallet.setFontSize(50);
|
||||
dialogPane.setGraphic(wallet);
|
||||
|
||||
final VBox content = new VBox(20);
|
||||
label = (CustomTextField) TextFields.createClearableTextField();
|
||||
label.setText(initialName);
|
||||
label.setTextFormatter(new TextFormatter<>((change) -> {
|
||||
change.setText(change.getText().replaceAll("[\\\\/:*?\"<>|]", "_"));
|
||||
return change;
|
||||
}));
|
||||
content.getChildren().add(label);
|
||||
|
||||
dialogPane.setContent(content);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(label, Validator.combine(
|
||||
Validator.createEmptyValidator("Account name is required")
|
||||
));
|
||||
});
|
||||
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rename Account", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button)dialogPane.lookupButton(okButtonType);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> label.getText().length() == 0, label.textProperty());
|
||||
okButton.disableProperty().bind(isInvalid);
|
||||
|
||||
label.setPromptText("Account Name");
|
||||
Platform.runLater(label::requestFocus);
|
||||
setResultConverter(dialogButton -> dialogButton == okButtonType ? label.getText() : null);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,18 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class KeystoresDiscoveredEvent {
|
||||
private final Map<StandardAccount, Keystore> discoveredKeystores;
|
||||
|
||||
public KeystoresDiscoveredEvent(Map<StandardAccount, Keystore> discoveredKeystores) {
|
||||
this.discoveredKeystores = discoveredKeystores;
|
||||
}
|
||||
|
||||
public Map<StandardAccount, Keystore> getDiscoveredKeystores() {
|
||||
return discoveredKeystores;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -10,8 +11,10 @@ public class SpendUtxoEvent {
|
||||
private final Wallet wallet;
|
||||
private final List<BlockTransactionHashIndex> utxos;
|
||||
private final List<Payment> payments;
|
||||
private final List<byte[]> opReturns;
|
||||
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);
|
||||
@@ -21,8 +24,20 @@ public class SpendUtxoEvent {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this.payments = payments;
|
||||
this.opReturns = null;
|
||||
this.fee = fee;
|
||||
this.includeSpentMempoolOutputs = includeSpentMempoolOutputs;
|
||||
this.pool = null;
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, Pool pool) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this.payments = payments;
|
||||
this.opReturns = opReturns;
|
||||
this.fee = fee;
|
||||
this.includeSpentMempoolOutputs = false;
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
@@ -37,6 +52,10 @@ public class SpendUtxoEvent {
|
||||
return payments;
|
||||
}
|
||||
|
||||
public List<byte[]> getOpReturns() {
|
||||
return opReturns;
|
||||
}
|
||||
|
||||
public Long getFee() {
|
||||
return fee;
|
||||
}
|
||||
@@ -44,4 +63,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,17 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
/**
|
||||
* This event is posted if the wallet's gap limit has changed, and triggers a history fetch for the new nodes.
|
||||
*
|
||||
*/
|
||||
public class WalletGapLimitChangedEvent extends WalletChangedEvent {
|
||||
public WalletGapLimitChangedEvent(Wallet wallet) {
|
||||
super(wallet);
|
||||
}
|
||||
|
||||
public int getGapLimit() {
|
||||
return getWallet().getGapLimit();
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,17 @@ package com.sparrowwallet.sparrow.event;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
|
||||
public class WalletHistoryStartedEvent extends WalletHistoryStatusEvent {
|
||||
private final WalletNode walletNode;
|
||||
import java.util.Set;
|
||||
|
||||
public WalletHistoryStartedEvent(Wallet wallet, WalletNode walletNode) {
|
||||
public class WalletHistoryStartedEvent extends WalletHistoryStatusEvent {
|
||||
private final Set<WalletNode> walletNodes;
|
||||
|
||||
public WalletHistoryStartedEvent(Wallet wallet, Set<WalletNode> walletNodes) {
|
||||
super(wallet, true);
|
||||
this.walletNode = walletNode;
|
||||
this.walletNodes = walletNodes;
|
||||
}
|
||||
|
||||
public WalletNode getWalletNode() {
|
||||
return walletNode;
|
||||
public Set<WalletNode> getWalletNodes() {
|
||||
return walletNodes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletLabelChangedEvent extends WalletChangedEvent {
|
||||
public WalletLabelChangedEvent(Wallet wallet) {
|
||||
super(wallet);
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return getWallet().getLabel();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +50,16 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
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);
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path));
|
||||
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);
|
||||
@@ -67,7 +73,6 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
wallet.getKeystores().add(keystore);
|
||||
}
|
||||
|
||||
ScriptType scriptType = ScriptType.valueOf(cf.addressType);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.MULTI, scriptType, wallet.getKeystores(), cf.quorum.requiredSigners));
|
||||
|
||||
@@ -95,7 +100,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
|
||||
try {
|
||||
CaravanFile cf = new CaravanFile();
|
||||
cf.name = wallet.getName();
|
||||
cf.name = wallet.getFullName();
|
||||
cf.addressType = wallet.getScriptType().toString().replace('-', '_');
|
||||
cf.network = Network.get().getName();
|
||||
cf.client = new Client();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.samourai.whirlpool.client.wallet.beans.IndexRange;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
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 +28,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;
|
||||
@@ -40,8 +41,9 @@ public class Config {
|
||||
private boolean openWalletsInNewWindows = false;
|
||||
private boolean hideEmptyUsedAddresses = false;
|
||||
private boolean showTransactionHex = true;
|
||||
private boolean showLoadingLog = false;
|
||||
private boolean showLoadingLog = true;
|
||||
private boolean showUtxosChart = true;
|
||||
private boolean preventSleep = false;
|
||||
private List<File> recentWalletFiles;
|
||||
private Integer keyDerivationPeriod;
|
||||
private File hwi;
|
||||
@@ -53,12 +55,12 @@ public class Config {
|
||||
private CoreAuthType coreAuthType;
|
||||
private File coreDataDir;
|
||||
private String coreAuth;
|
||||
private Boolean coreMultiWallet;
|
||||
private String coreWallet;
|
||||
private String electrumServer;
|
||||
private File electrumServerCert;
|
||||
private boolean useProxy;
|
||||
private String proxyServer;
|
||||
private Double appWidth;
|
||||
private Double appHeight;
|
||||
|
||||
private static Config INSTANCE;
|
||||
|
||||
@@ -138,6 +140,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public OptimizationStrategy getSendOptimizationStrategy() {
|
||||
return sendOptimizationStrategy;
|
||||
}
|
||||
|
||||
public void setSendOptimizationStrategy(OptimizationStrategy sendOptimizationStrategy) {
|
||||
this.sendOptimizationStrategy = sendOptimizationStrategy;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Currency getFiatCurrency() {
|
||||
return fiatCurrency;
|
||||
}
|
||||
@@ -268,6 +279,14 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isPreventSleep() {
|
||||
return preventSleep;
|
||||
}
|
||||
|
||||
public void setPreventSleep(boolean preventSleep) {
|
||||
this.preventSleep = preventSleep;
|
||||
}
|
||||
|
||||
public List<File> getRecentWalletFiles() {
|
||||
return recentWalletFiles;
|
||||
}
|
||||
@@ -405,24 +424,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;
|
||||
}
|
||||
@@ -459,6 +460,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 {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Descriptor implements WalletImport, WalletExport {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Output Descriptor";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.BITCOIN_CORE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
try {
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
|
||||
String outputDescriptorString = outputDescriptor.toString(true);
|
||||
outputStream.write(outputDescriptorString.getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.flush();
|
||||
} catch(Exception e) {
|
||||
throw new ExportException("Error writing output descriptor", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "The output descriptor is a standardized description of the wallet compatible with Bitcoin Core, and can be used to create a watch-only copy using the Edit button on the Settings tab of a new Sparrow wallet.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
return "txt";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean walletExportRequiresDecryption() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted(File file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return "Import a file containing the output descriptor of a wallet. The output descriptor is a standardized description of the wallet compatible with Bitcoin Core.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
String outputDescriptor = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n"));
|
||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(outputDescriptor.trim());
|
||||
return descriptor.toWallet();
|
||||
} catch(Exception e) {
|
||||
throw new ImportException("Error importing output descriptor", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
@@ -91,6 +92,15 @@ public class Hwi {
|
||||
}
|
||||
}
|
||||
|
||||
public Map<StandardAccount, String> getXpubs(Device device, String passphrase, Map<StandardAccount, String> accountDerivationPaths) throws ImportException {
|
||||
Map<StandardAccount, String> accountXpubs = new LinkedHashMap<>();
|
||||
for(Map.Entry<StandardAccount, String> entry : accountDerivationPaths.entrySet()) {
|
||||
accountXpubs.put(entry.getKey(), getXpub(device, passphrase, entry.getValue()));
|
||||
}
|
||||
|
||||
return accountXpubs;
|
||||
}
|
||||
|
||||
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
|
||||
try {
|
||||
String output;
|
||||
@@ -580,6 +590,28 @@ public class Hwi {
|
||||
}
|
||||
}
|
||||
|
||||
public static class GetXpubsService extends Service<Map<StandardAccount, String>> {
|
||||
private final Device device;
|
||||
private final String passphrase;
|
||||
private final Map<StandardAccount, String> accountDerivationPaths;
|
||||
|
||||
public GetXpubsService(Device device, String passphrase, Map<StandardAccount, String> accountDerivationPaths) {
|
||||
this.device = device;
|
||||
this.passphrase = passphrase;
|
||||
this.accountDerivationPaths = accountDerivationPaths;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Map<StandardAccount, String>> createTask() {
|
||||
return new Task<>() {
|
||||
protected Map<StandardAccount, String> call() throws ImportException {
|
||||
Hwi hwi = new Hwi();
|
||||
return hwi.getXpubs(device, passphrase, accountDerivationPaths);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class SignPSBTService extends Service<PSBT> {
|
||||
private final Device device;
|
||||
private final String passphrase;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public interface BlockTransactionDao {
|
||||
@@ -35,7 +36,8 @@ public interface BlockTransactionDao {
|
||||
void clear(long wallet);
|
||||
|
||||
default void addBlockTransactions(Wallet wallet) {
|
||||
for(Map.Entry<Sha256Hash, BlockTransaction> blkTxEntry : wallet.getTransactions().entrySet()) {
|
||||
Map<Sha256Hash, BlockTransaction> walletTransactions = new HashMap<>(wallet.getTransactions());
|
||||
for(Map.Entry<Sha256Hash, BlockTransaction> blkTxEntry : walletTransactions.entrySet()) {
|
||||
blkTxEntry.getValue().setId(null);
|
||||
addOrUpdate(wallet, blkTxEntry.getKey(), blkTxEntry.getValue());
|
||||
}
|
||||
@@ -45,16 +47,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 {
|
||||
@@ -227,10 +236,18 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
if(dirtyPersistables.label != null) {
|
||||
walletDao.updateLabel(wallet.getId(), dirtyPersistables.label.length() > 255 ? dirtyPersistables.label.substring(0, 255) : dirtyPersistables.label);
|
||||
}
|
||||
|
||||
if(dirtyPersistables.blockHeight != null) {
|
||||
walletDao.updateStoredBlockHeight(wallet.getId(), dirtyPersistables.blockHeight);
|
||||
}
|
||||
|
||||
if(dirtyPersistables.gapLimit != null) {
|
||||
walletDao.updateGapLimit(wallet.getId(), dirtyPersistables.gapLimit);
|
||||
}
|
||||
|
||||
if(!dirtyPersistables.labelEntries.isEmpty()) {
|
||||
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class);
|
||||
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class);
|
||||
@@ -264,6 +281,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) {
|
||||
@@ -399,6 +434,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);
|
||||
@@ -605,6 +645,13 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletLabelChanged(WalletLabelChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).label = event.getLabel();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletBlockHeightChanged(WalletBlockHeightChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
@@ -612,6 +659,13 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletGapLimitChanged(WalletGapLimitChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).gapLimit = event.getGapLimit();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletEntryLabelsChanged(WalletEntryLabelsChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
@@ -626,6 +680,21 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void keystoreLabelsChanged(KeystoreLabelsChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
@@ -643,9 +712,14 @@ public class DbPersistence implements Persistence {
|
||||
private static class DirtyPersistables {
|
||||
public boolean clearHistory;
|
||||
public final List<WalletNode> historyNodes = new ArrayList<>();
|
||||
public String label;
|
||||
public Integer blockHeight = null;
|
||||
public Integer gapLimit = 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<>();
|
||||
|
||||
@@ -653,11 +727,16 @@ public class DbPersistence implements Persistence {
|
||||
return "Dirty Persistables" +
|
||||
"\nClear history:" + clearHistory +
|
||||
"\nNodes:" + historyNodes +
|
||||
"\nLabel:" + label +
|
||||
"\nBlockHeight:" + blockHeight +
|
||||
"\nGap limit:" + gapLimit +
|
||||
"\nTx labels:" + labelEntries.stream().filter(entry -> entry instanceof TransactionEntry).map(entry -> ((TransactionEntry)entry).getBlockTransaction().getHash().toString()).collect(Collectors.toList()) +
|
||||
"\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, indexRange, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex from mixConfig where wallet = ?")
|
||||
@RegisterRowMapper(MixConfigMapper.class)
|
||||
MixConfig getForWalletId(Long id);
|
||||
|
||||
@SqlUpdate("insert into mixConfig (scode, mixOnStartup, indexRange, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex, wallet) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insertMixConfig(String scode, Boolean mixOnStartup, String indexRange, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet);
|
||||
|
||||
@SqlUpdate("update mixConfig set scode = ?, mixOnStartup = ?, indexRange = ?, mixToWalletFile = ?, mixToWalletName = ?, minMixes = ?, receiveIndex = ?, changeIndex = ?, wallet = ? where id = ?")
|
||||
void updateMixConfig(String scode, Boolean mixOnStartup, String indexRange, 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(), mixConfig.getIndexRange(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId());
|
||||
mixConfig.setId(id);
|
||||
} else {
|
||||
updateMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixConfig.getIndexRange(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId(), mixConfig.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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 indexRange = rs.getString("indexRange");
|
||||
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, indexRange, 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,25 +30,37 @@ public interface WalletDao {
|
||||
@CreateSqlObject
|
||||
BlockTransactionDao createBlockTransactionDao();
|
||||
|
||||
@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")
|
||||
@CreateSqlObject
|
||||
MixConfigDao createMixConfigDao();
|
||||
|
||||
@CreateSqlObject
|
||||
UtxoMixDataDao createUtxoMixDataDao();
|
||||
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.label, 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();
|
||||
|
||||
@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 where wallet.id = 1")
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.label, 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 where wallet.id = 1")
|
||||
@RegisterRowMapper(WalletMapper.class)
|
||||
Wallet loadMainWallet();
|
||||
|
||||
@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 where wallet.id != 1")
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.label, 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 where wallet.id != 1")
|
||||
@RegisterRowMapper(WalletMapper.class)
|
||||
List<Wallet> loadChildWallets();
|
||||
|
||||
@SqlUpdate("insert into wallet (name, network, policyType, scriptType, storedBlockHeight, gapLimit, birthDate, defaultPolicy) values (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@SqlUpdate("insert into wallet (name, label, network, policyType, scriptType, storedBlockHeight, gapLimit, birthDate, defaultPolicy) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insert(String name, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Date birthDate, long defaultPolicy);
|
||||
long insert(String name, String label, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Date birthDate, long defaultPolicy);
|
||||
|
||||
@SqlUpdate("update wallet set label = :label where id = :id")
|
||||
void updateLabel(@Bind("id") long id, @Bind("label") String label);
|
||||
|
||||
@SqlUpdate("update wallet set storedBlockHeight = :blockHeight where id = :id")
|
||||
void updateStoredBlockHeight(@Bind("id") long id, @Bind("blockHeight") Integer blockHeight);
|
||||
|
||||
@SqlUpdate("update wallet set gapLimit = :gapLimit where id = :id")
|
||||
void updateGapLimit(@Bind("id") long id, @Bind("gapLimit") Integer gapLimit);
|
||||
|
||||
@SqlUpdate("set schema ?")
|
||||
int setSchema(String schema);
|
||||
|
||||
@@ -86,6 +99,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 +111,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()), truncate(wallet.getLabel()), 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ public class WalletMapper implements RowMapper<Wallet> {
|
||||
public Wallet map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
Wallet wallet = new Wallet(rs.getString("wallet.name"));
|
||||
wallet.setId(rs.getLong("wallet.id"));
|
||||
wallet.setLabel(rs.getString("wallet.label"));
|
||||
wallet.setNetwork(Network.values()[rs.getInt("wallet.network")]);
|
||||
wallet.setPolicyType(PolicyType.values()[rs.getInt("wallet.policyType")]);
|
||||
wallet.setScriptType(ScriptType.values()[rs.getInt("wallet.scriptType")]);
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
import org.jdbi.v3.sqlobject.statement.UseRowReducer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@@ -58,10 +59,11 @@ 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);
|
||||
List<WalletNode> childNodes = new ArrayList<>(purposeNode.getChildren());
|
||||
for(WalletNode addressNode : childNodes) {
|
||||
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), truncate(addressNode.getLabel()), wallet.getId(), purposeNodeId);
|
||||
addressNode.setId(addressNodeId);
|
||||
addTransactionOutputs(addressNode);
|
||||
}
|
||||
@@ -84,22 +86,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 +115,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) {
|
||||
|
||||
@@ -5,8 +5,6 @@ import com.github.arteam.simplejsonrpc.client.Transport;
|
||||
import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder;
|
||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException;
|
||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -19,6 +17,8 @@ import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.drongo.wallet.WalletNode.nodeRangesToString;
|
||||
|
||||
public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
private static final Logger log = LoggerFactory.getLogger(BatchedElectrumServerRpc.class);
|
||||
private static final int MAX_RETRIES = 5;
|
||||
@@ -75,7 +75,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
public Map<String, ScriptHashTx[]> getScriptHashHistory(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes, boolean failOnError) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class);
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + getScriptHashesAbbreviation(pathScriptHashes.keySet())));
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + nodeRangesToString(pathScriptHashes.keySet())));
|
||||
|
||||
for(String path : pathScriptHashes.keySet()) {
|
||||
batchRequest.add(path, "blockchain.scripthash.get_history", pathScriptHashes.get(path));
|
||||
@@ -85,7 +85,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
return new RetryLogic<Map<String, ScriptHashTx[]>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
|
||||
} catch (JsonRpcBatchException e) {
|
||||
if(failOnError) {
|
||||
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + getScriptHashesAbbreviation((Collection<String>)e.getErrors().keySet()), e);
|
||||
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + nodeRangesToString((Collection<String>)e.getErrors().keySet()), e);
|
||||
}
|
||||
|
||||
Map<String, ScriptHashTx[]> result = (Map<String, ScriptHashTx[]>)e.getSuccesses();
|
||||
@@ -95,7 +95,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
|
||||
return result;
|
||||
} catch(Exception e) {
|
||||
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + getScriptHashesAbbreviation(pathScriptHashes.keySet()), e);
|
||||
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + nodeRangesToString(pathScriptHashes.keySet()), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
return new RetryLogic<Map<String, ScriptHashTx[]>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
|
||||
} catch(JsonRpcBatchException e) {
|
||||
if(failOnError) {
|
||||
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + getScriptHashesAbbreviation((Collection<String>)e.getErrors().keySet()), e);
|
||||
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + nodeRangesToString((Collection<String>)e.getErrors().keySet()), e);
|
||||
}
|
||||
|
||||
Map<String, ScriptHashTx[]> result = (Map<String, ScriptHashTx[]>)e.getSuccesses();
|
||||
@@ -123,7 +123,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
|
||||
return result;
|
||||
} catch(Exception e) {
|
||||
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + getScriptHashesAbbreviation(pathScriptHashes.keySet()), e);
|
||||
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + nodeRangesToString(pathScriptHashes.keySet()), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
public Map<String, String> subscribeScriptHashes(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + getScriptHashesAbbreviation(pathScriptHashes.keySet())));
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + nodeRangesToString(pathScriptHashes.keySet())));
|
||||
|
||||
for(String path : pathScriptHashes.keySet()) {
|
||||
batchRequest.add(path, "blockchain.scripthash.subscribe", pathScriptHashes.get(path));
|
||||
@@ -142,9 +142,9 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
return new RetryLogic<Map<String, String>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
|
||||
} catch(JsonRpcBatchException e) {
|
||||
//Even if we have some successes, failure to subscribe for all script hashes will result in outdated wallet view. Don't proceed.
|
||||
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + getScriptHashesAbbreviation((Collection<String>)e.getErrors().keySet()), e);
|
||||
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + nodeRangesToString((Collection<String>)e.getErrors().keySet()), e);
|
||||
} catch(Exception e) {
|
||||
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + getScriptHashesAbbreviation(pathScriptHashes.keySet()), e);
|
||||
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + nodeRangesToString(pathScriptHashes.keySet()), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,65 +276,4 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
throw new ElectrumServerRpcException("Error broadcasting transaction", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getScriptHashesAbbreviation(Collection<String> scriptHashes) {
|
||||
List<String> sortedHashes = new ArrayList<>(scriptHashes);
|
||||
|
||||
if(scriptHashes.isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
|
||||
List<List<String>> contiguous = splitToContiguous(sortedHashes);
|
||||
|
||||
String abbrev = "[";
|
||||
for(Iterator<List<String>> iter = contiguous.iterator(); iter.hasNext(); ) {
|
||||
List<String> range = iter.next();
|
||||
abbrev += range.get(0);
|
||||
if(range.size() > 1) {
|
||||
abbrev += "-" + range.get(range.size() - 1);
|
||||
}
|
||||
if(iter.hasNext()) {
|
||||
abbrev += ", ";
|
||||
}
|
||||
}
|
||||
abbrev += "]";
|
||||
|
||||
return abbrev;
|
||||
}
|
||||
|
||||
static List<List<String>> splitToContiguous(List<String> input) {
|
||||
List<List<String>> result = new ArrayList<>();
|
||||
int prev = 0;
|
||||
|
||||
int keyPurpose = getKeyPurpose(input.get(0));
|
||||
int index = getIndex(input.get(0));
|
||||
|
||||
for (int cur = 0; cur < input.size(); cur++) {
|
||||
if(getKeyPurpose(input.get(cur)) != keyPurpose || getIndex(input.get(cur)) != index) {
|
||||
result.add(input.subList(prev, cur));
|
||||
prev = cur;
|
||||
}
|
||||
index = getIndex(input.get(cur)) + 1;
|
||||
keyPurpose = getKeyPurpose(input.get(cur));
|
||||
}
|
||||
result.add(input.subList(prev, input.size()));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int getKeyPurpose(String path) {
|
||||
List<ChildNumber> childNumbers = KeyDerivation.parsePath(path);
|
||||
if(childNumbers.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
return childNumbers.get(0).num();
|
||||
}
|
||||
|
||||
private static int getIndex(String path) {
|
||||
List<ChildNumber> childNumbers = KeyDerivation.parsePath(path);
|
||||
if(childNumbers.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
return childNumbers.get(childNumbers.size() - 1).num();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,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;
|
||||
@@ -93,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);
|
||||
@@ -250,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) {
|
||||
@@ -391,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) {
|
||||
@@ -696,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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -762,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);
|
||||
@@ -799,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);
|
||||
}
|
||||
@@ -848,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) {
|
||||
@@ -1229,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;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1326,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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1367,4 +1415,48 @@ public class ElectrumServer {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class WalletDiscoveryService extends Service<List<StandardAccount>> {
|
||||
private final Wallet masterWalletCopy;
|
||||
private final List<StandardAccount> standardAccounts;
|
||||
private final Map<StandardAccount, Keystore> importedKeystores;
|
||||
|
||||
public WalletDiscoveryService(Wallet masterWallet, List<StandardAccount> standardAccounts) {
|
||||
this.masterWalletCopy = masterWallet.copy();
|
||||
this.standardAccounts = standardAccounts;
|
||||
this.importedKeystores = new HashMap<>();
|
||||
}
|
||||
|
||||
public WalletDiscoveryService(Wallet masterWallet, Map<StandardAccount, Keystore> importedKeystores) {
|
||||
this.masterWalletCopy = masterWallet.copy();
|
||||
this.standardAccounts = new ArrayList<>(importedKeystores.keySet());
|
||||
this.importedKeystores = importedKeystores;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<List<StandardAccount>> createTask() {
|
||||
return new Task<>() {
|
||||
protected List<StandardAccount> call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
List<StandardAccount> discoveredAccounts = new ArrayList<>();
|
||||
|
||||
for(StandardAccount standardAccount : standardAccounts) {
|
||||
Wallet wallet = masterWalletCopy.addChildWallet(standardAccount);
|
||||
if(importedKeystores.containsKey(standardAccount)) {
|
||||
wallet.getKeystores().clear();
|
||||
wallet.getKeystores().add(importedKeystores.get(standardAccount));
|
||||
}
|
||||
|
||||
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>();
|
||||
electrumServer.getReferences(wallet, wallet.getNode(KeyPurpose.RECEIVE).getChildren(), nodeTransactionMap, 0);
|
||||
if(nodeTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
|
||||
discoveredAccounts.add(standardAccount);
|
||||
}
|
||||
}
|
||||
|
||||
return discoveredAccounts;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
/**
|
||||
* Matches a request based on IP Address or subnet mask matching against the remote
|
||||
* address.
|
||||
* <p>
|
||||
* Both IPv6 and IPv4 addresses are supported, but a matcher which is configured with an
|
||||
* IPv4 address will never match a request which returns an IPv6 address, and vice-versa.
|
||||
*
|
||||
* @author Luke Taylor
|
||||
* @since 3.0.2
|
||||
*
|
||||
* Slightly modified by omidzk to have zero dependency to any frameworks other than the JRE.
|
||||
*/
|
||||
public final class IpAddressMatcher {
|
||||
private static final IpAddressMatcher LOCAL_RANGE_1 = new IpAddressMatcher("10.0.0.0/8");
|
||||
private static final IpAddressMatcher LOCAL_RANGE_2 = new IpAddressMatcher("172.16.0.0/12");
|
||||
private static final IpAddressMatcher LOCAL_RANGE_3 = new IpAddressMatcher("192.168.0.0/16");
|
||||
|
||||
private final int nMaskBits;
|
||||
private final InetAddress requiredAddress;
|
||||
|
||||
/**
|
||||
* Takes a specific IP address or a range specified using the IP/Netmask (e.g.
|
||||
* 192.168.1.0/24 or 202.24.0.0/14).
|
||||
*
|
||||
* @param ipAddress the address or range of addresses from which the request must
|
||||
* come.
|
||||
*/
|
||||
public IpAddressMatcher(String ipAddress) {
|
||||
|
||||
if (ipAddress.indexOf('/') > 0) {
|
||||
String[] addressAndMask = ipAddress.split("/");
|
||||
ipAddress = addressAndMask[0];
|
||||
nMaskBits = Integer.parseInt(addressAndMask[1]);
|
||||
}
|
||||
else {
|
||||
nMaskBits = -1;
|
||||
}
|
||||
requiredAddress = parseAddress(ipAddress);
|
||||
assert (requiredAddress.getAddress().length * 8 >= nMaskBits) :
|
||||
String.format("IP address %s is too short for bitmask of length %d",
|
||||
ipAddress, nMaskBits);
|
||||
}
|
||||
|
||||
public boolean matches(String address) {
|
||||
InetAddress remoteAddress = parseAddress(address);
|
||||
|
||||
if (!requiredAddress.getClass().equals(remoteAddress.getClass())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nMaskBits < 0) {
|
||||
return remoteAddress.equals(requiredAddress);
|
||||
}
|
||||
|
||||
byte[] remAddr = remoteAddress.getAddress();
|
||||
byte[] reqAddr = requiredAddress.getAddress();
|
||||
|
||||
int nMaskFullBytes = nMaskBits / 8;
|
||||
byte finalByte = (byte) (0xFF00 >> (nMaskBits & 0x07));
|
||||
|
||||
for (int i = 0; i < nMaskFullBytes; i++) {
|
||||
if (remAddr[i] != reqAddr[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalByte != 0) {
|
||||
return (remAddr[nMaskFullBytes] & finalByte) == (reqAddr[nMaskFullBytes] & finalByte);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private InetAddress parseAddress(String address) {
|
||||
try {
|
||||
return InetAddress.getByName(address);
|
||||
}
|
||||
catch (UnknownHostException e) {
|
||||
throw new IllegalArgumentException("Failed to parse address" + address, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isLocalNetworkAddress(String address) {
|
||||
return "localhost".equals(address) || "127.0.0.1".equals(address) || LOCAL_RANGE_1.matches(address) || LOCAL_RANGE_2.matches(address) || LOCAL_RANGE_3.matches(address);
|
||||
}
|
||||
}
|
||||
@@ -116,10 +116,21 @@ public enum Protocol {
|
||||
return toUrlString() + hostAndPort.toString();
|
||||
}
|
||||
|
||||
public boolean isOnionAddress(HostAndPort server) {
|
||||
public static boolean isOnionAddress(HostAndPort server) {
|
||||
return server.getHost().toLowerCase().endsWith(TorService.TOR_ADDRESS_SUFFIX);
|
||||
}
|
||||
|
||||
public static boolean isOnionAddress(String address) {
|
||||
if(address != null) {
|
||||
Protocol protocol = Protocol.getProtocol(address);
|
||||
if(protocol != null) {
|
||||
return isOnionAddress(protocol.getServerHostAndPort(address));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Protocol getProtocol(String url) {
|
||||
if(url.startsWith("tcp://")) {
|
||||
return TCP;
|
||||
|
||||
@@ -26,11 +26,13 @@ public class TcpTransport implements Transport, Closeable {
|
||||
|
||||
public static final int DEFAULT_PORT = 50001;
|
||||
private static final int[] BASE_READ_TIMEOUT_SECS = {3, 8, 16, 34};
|
||||
private static final int[] SLOW_READ_TIMEOUT_SECS = {34, 68, 124, 208};
|
||||
public static final long PER_REQUEST_READ_TIMEOUT_MILLIS = 50;
|
||||
public static final int SOCKET_READ_TIMEOUT_MILLIS = 5000;
|
||||
|
||||
protected final HostAndPort server;
|
||||
protected final SocketFactory socketFactory;
|
||||
protected final int[] readTimeouts;
|
||||
|
||||
private Socket socket;
|
||||
|
||||
@@ -59,6 +61,7 @@ public class TcpTransport implements Transport, Closeable {
|
||||
public TcpTransport(HostAndPort server, HostAndPort proxy) {
|
||||
this.server = server;
|
||||
this.socketFactory = (proxy == null ? SocketFactory.getDefault() : new ProxySocketFactory(proxy));
|
||||
this.readTimeouts = (Config.get().getServerType() == ServerType.BITCOIN_CORE && Protocol.isOnionAddress(Config.get().getCoreServer()) ? SLOW_READ_TIMEOUT_SECS : BASE_READ_TIMEOUT_SECS);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,16 +94,16 @@ public class TcpTransport implements Transport, Closeable {
|
||||
|
||||
private String readResponse() throws IOException {
|
||||
try {
|
||||
if(!readLock.tryLock((BASE_READ_TIMEOUT_SECS[readTimeoutIndex] * 1000) + (requestIdCount * PER_REQUEST_READ_TIMEOUT_MILLIS), TimeUnit.MILLISECONDS)) {
|
||||
readTimeoutIndex = Math.min(readTimeoutIndex + 1, BASE_READ_TIMEOUT_SECS.length - 1);
|
||||
log.info("No response from server, setting read timeout to " + BASE_READ_TIMEOUT_SECS[readTimeoutIndex] + " secs");
|
||||
if(!readLock.tryLock((readTimeouts[readTimeoutIndex] * 1000L) + (requestIdCount * PER_REQUEST_READ_TIMEOUT_MILLIS), TimeUnit.MILLISECONDS)) {
|
||||
readTimeoutIndex = Math.min(readTimeoutIndex + 1, readTimeouts.length - 1);
|
||||
log.warn("No response from server, setting read timeout to " + readTimeouts[readTimeoutIndex] + " secs");
|
||||
throw new IOException("No response from server");
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
throw new IOException("Read thread interrupted");
|
||||
}
|
||||
|
||||
if(readTimeoutIndex == BASE_READ_TIMEOUT_SECS.length - 1) {
|
||||
if(readTimeoutIndex == readTimeouts.length - 1) {
|
||||
readTimeoutIndex--;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,14 @@ package com.sparrowwallet.sparrow.net;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import net.freehaven.tor.control.TorControlError;
|
||||
import org.berndpruenster.netlayer.tor.NativeTor;
|
||||
import org.berndpruenster.netlayer.tor.Tor;
|
||||
import org.berndpruenster.netlayer.tor.TorCtlException;
|
||||
import org.berndpruenster.netlayer.tor.Torrc;
|
||||
import org.berndpruenster.netlayer.tor.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.Socket;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
@@ -40,6 +39,7 @@ public class TorService extends ScheduledService<NativeTor> {
|
||||
try {
|
||||
LinkedHashMap<String, String> torrcOptionsMap = new LinkedHashMap<>();
|
||||
torrcOptionsMap.put("SocksPort", Integer.toString(PROXY_PORT));
|
||||
torrcOptionsMap.put("HashedControlPassword", "16:D780432418F09B06609940000924317D3B9DF522A3191F8F4E597E9329");
|
||||
torrcOptionsMap.put("DisableNetwork", "0");
|
||||
Torrc override = new Torrc(torrcOptionsMap);
|
||||
|
||||
@@ -62,4 +62,25 @@ public class TorService extends ScheduledService<NativeTor> {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Socket getControlSocket() {
|
||||
Tor tor = Tor.getDefault();
|
||||
if(tor != null) {
|
||||
try {
|
||||
Class<?> torClass = Class.forName("org.berndpruenster.netlayer.tor.Tor");
|
||||
Field torControllerField = torClass.getDeclaredField("torController");
|
||||
torControllerField.setAccessible(true);
|
||||
TorController torController = (TorController)torControllerField.get(tor);
|
||||
|
||||
Class<?> torControllerClass = Class.forName("org.berndpruenster.netlayer.tor.TorController");
|
||||
Field socketField = torControllerClass.getDeclaredField("socket");
|
||||
socketField.setAccessible(true);
|
||||
return (Socket)socketField.get(torController);
|
||||
} catch(Exception e) {
|
||||
log.error("Error retrieving Tor control socket", e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user