mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-30 19:46:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f75b4582c8 | ||
|
|
5d823571df | ||
|
|
cca61d281c | ||
|
|
34c9bc9b69 | ||
|
|
5fa048d242 | ||
|
|
ca928fc136 | ||
|
|
9bf53ab0cd | ||
|
|
542cc7de6f | ||
|
|
77fde3cda9 | ||
|
|
1eb595823b | ||
|
|
6d2167428f | ||
|
|
3820b9838d | ||
|
|
20a99e3236 | ||
|
|
c81f3d9f5d | ||
|
|
526de33bdd | ||
|
|
ee992b7255 | ||
|
|
7d459a9115 | ||
|
|
9faf036e4d | ||
|
|
306f241a4a | ||
|
|
41d1a1806d | ||
|
|
a825a693c1 | ||
|
|
7423d94935 | ||
|
|
82f9a0f2af | ||
|
|
7da62bb135 | ||
|
|
f2e5259916 | ||
|
|
4f4a48eb98 | ||
|
|
9c3b647f07 | ||
|
|
7f2d72ee59 | ||
|
|
548b8d270b | ||
|
|
696746f06c | ||
|
|
6f11a20feb | ||
|
|
c28c2ed506 | ||
|
|
8fb7f544de | ||
|
|
a76d9dba21 | ||
|
|
f1b3f7d5dd | ||
|
|
fd9e19d052 | ||
|
|
56363c083e | ||
|
|
bbdfec127a | ||
|
|
eb1087bf8d | ||
|
|
796f68640c | ||
|
|
ad091217d6 | ||
|
|
40e06b96a9 | ||
|
|
3fd186e22c | ||
|
|
e8c7f57704 | ||
|
|
e6de33034b | ||
|
|
73b6b9219b | ||
|
|
d1d1b0806f | ||
|
|
5ee97966ee | ||
|
|
4819f329ae | ||
|
|
52aed8a3f5 | ||
|
|
86ef129c1b | ||
|
|
aff872eea0 | ||
|
|
5d0025b4a7 | ||
|
|
e625a4e542 | ||
|
|
72ff1df61e | ||
|
|
3776fbafd9 | ||
|
|
cd91aff3bb | ||
|
|
b530ced9ed | ||
|
|
22957e9d88 | ||
|
|
d76aecb34d | ||
|
|
4cbb402931 | ||
|
|
4da82b110c | ||
|
|
880096a193 | ||
|
|
06c0fb8594 | ||
|
|
8e85543c6e | ||
|
|
4fb72fdf89 | ||
|
|
cf2616ec2b | ||
|
|
238aae5ea9 | ||
|
|
a963d10381 | ||
|
|
eb90d6a31a | ||
|
|
b013b5f50f | ||
|
|
14db333a6f | ||
|
|
6804f713b2 | ||
|
|
90a2c3b89b | ||
|
|
3c94664ac3 | ||
|
|
1b61a78e6d | ||
|
|
1defe51fd7 | ||
|
|
4cbd778ca1 | ||
|
|
e59ee47624 | ||
|
|
8b42399423 | ||
|
|
e84f82f47b | ||
|
|
e4189711bd | ||
|
|
9bca911b0b | ||
|
|
eb498f2bcc | ||
|
|
3d13f596a0 | ||
|
|
28984427e1 | ||
|
|
ed92acc468 | ||
|
|
3b9c87abc7 | ||
|
|
00181875c1 | ||
|
|
26fb2b97fb | ||
|
|
44194a074c | ||
|
|
4edd84f6e2 | ||
|
|
0956c96046 | ||
|
|
3013688447 | ||
|
|
0302913c3f | ||
|
|
72768362a5 | ||
|
|
39fa65ea37 | ||
|
|
4554c9d0df | ||
|
|
9b9b295045 | ||
|
|
ece786131e | ||
|
|
dc65313313 | ||
|
|
cb41a1ed66 | ||
|
|
c566dea232 | ||
|
|
8f04d23b3f | ||
|
|
b8b1039ada | ||
|
|
a7aafa27d0 | ||
|
|
18a1e82dda | ||
|
|
e26587e807 | ||
|
|
c3778b6419 | ||
|
|
fb85277894 | ||
|
|
8ae3399d56 | ||
|
|
7272de90f0 | ||
|
|
98c1f05ed6 | ||
|
|
90439501ad | ||
|
|
1bb3833cbe | ||
|
|
d7d8140050 | ||
|
|
3c77552211 | ||
|
|
5e7d1d1f69 | ||
|
|
8f92f9ec38 | ||
|
|
af23f063f2 | ||
|
|
13a3ce23e7 | ||
|
|
d7ff73243c | ||
|
|
25dd0440f6 | ||
|
|
b5301c4556 | ||
|
|
a22f69e2c1 | ||
|
|
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 | ||
|
|
90dea201b9 |
@@ -0,0 +1,2 @@
|
||||
Craig Raw <craigraw@gmail.com> Craig Raw <craig@quirk.biz>
|
||||
Craig Raw <craigraw@gmail.com> craigraw <craigraw@gmail.com>
|
||||
@@ -19,7 +19,12 @@ or for those without SSH credentials:
|
||||
In order to build, Sparrow requires Java 16 to be installed.
|
||||
The release binaries are built with [AdoptOpenJdk 16.0.1+9 Hotspot](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
|
||||
|
||||
These binaries can be built from source using
|
||||
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`
|
||||
|
||||
@@ -29,9 +34,11 @@ When updating to the latest HEAD
|
||||
|
||||
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`
|
||||
|
||||
|
||||
+42
-18
@@ -1,11 +1,11 @@
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.10'
|
||||
id 'extra-java-module-info'
|
||||
id 'com.dua3.javafxgradle7plugin' version '0.0.9'
|
||||
id 'org.beryx.jlink' version '2.24.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.5.0'
|
||||
def sparrowVersion = '1.5.6'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -29,7 +29,7 @@ tasks.withType(AbstractArchiveTask) {
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = "16"
|
||||
version = "17"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
@@ -45,13 +45,13 @@ dependencies {
|
||||
}
|
||||
implementation('com.google.guava:guava:28.2-jre')
|
||||
implementation('com.google.code.gson:gson:2.8.6')
|
||||
implementation('com.h2database:h2:1.4.201-SNAPSHOT')
|
||||
implementation('com.h2database:h2:2.0.206')
|
||||
implementation('com.zaxxer:HikariCP:4.0.3')
|
||||
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0')
|
||||
implementation('org.flywaydb:flyway-core:7.10.5-SNAPSHOT')
|
||||
implementation('org.flywaydb:flyway-core:7.10.7-SNAPSHOT')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||
implementation('com.google.zxing:javase:3.4.0') {
|
||||
@@ -65,7 +65,8 @@ dependencies {
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.2')
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.4')
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
}
|
||||
@@ -91,7 +92,11 @@ dependencies {
|
||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.17-SNAPSHOT')
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.27')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||
testImplementation('junit:junit:4.12')
|
||||
}
|
||||
|
||||
@@ -122,7 +127,8 @@ test {
|
||||
}
|
||||
|
||||
run {
|
||||
applicationDefaultJvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
||||
@@ -142,7 +148,7 @@ run {
|
||||
"--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"]
|
||||
}
|
||||
}
|
||||
@@ -191,10 +197,11 @@ jlink {
|
||||
"--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"]
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
|
||||
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor"]
|
||||
|
||||
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")
|
||||
@@ -204,14 +211,14 @@ jlink {
|
||||
appVersion = "${sparrowVersion}"
|
||||
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']
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--license-file', 'LICENSE']
|
||||
if(os.windows) {
|
||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
||||
installerType = "exe"
|
||||
}
|
||||
if(os.linux) {
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-rpm-license-type', 'ASL 2.0']
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow', '--linux-rpm-license-type', 'ASL 2.0']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
||||
}
|
||||
if(os.macOsX) {
|
||||
@@ -354,7 +361,7 @@ extraJavaModuleInfo {
|
||||
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
|
||||
exports('org.json.simple')
|
||||
}
|
||||
module('logback-classic-1.2.3.jar', 'logback.classic', '1.2.3') {
|
||||
module('logback-classic-1.2.8.jar', 'logback.classic', '1.2.8') {
|
||||
exports('ch.qos.logback.classic')
|
||||
requires('org.slf4j')
|
||||
requires('logback.core')
|
||||
@@ -431,6 +438,11 @@ extraJavaModuleInfo {
|
||||
requires('javafx.graphics')
|
||||
requires('javafx.controls')
|
||||
}
|
||||
module('rxjavafx-2.2.2.jar', 'io.reactivex.rxjava2fx', '2.2.2') {
|
||||
exports('io.reactivex.rxjavafx.schedulers')
|
||||
requires('io.reactivex.rxjava2')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
|
||||
requires('javafx.base')
|
||||
requires('javafx.graphics')
|
||||
@@ -441,7 +453,7 @@ extraJavaModuleInfo {
|
||||
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
|
||||
exports('kotlin')
|
||||
}
|
||||
module('hummingbird-1.6.2.jar', 'com.sparrowwallet.hummingbird', '1.6.2') {
|
||||
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
|
||||
exports('com.sparrowwallet.hummingbird')
|
||||
exports('com.sparrowwallet.hummingbird.registry')
|
||||
requires('co.nstant.in.cbor')
|
||||
@@ -449,7 +461,7 @@ extraJavaModuleInfo {
|
||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||
exports('co.nstant.in.cbor')
|
||||
}
|
||||
module('nightjar-0.2.17-SNAPSHOT.jar', 'com.sparrowwallet.nightjar', '0.2.17-SNAPSHOT') {
|
||||
module('nightjar-0.2.27.jar', 'com.sparrowwallet.nightjar', '0.2.27') {
|
||||
requires('com.google.common')
|
||||
requires('net.sourceforge.streamsupport')
|
||||
requires('org.slf4j')
|
||||
@@ -459,6 +471,7 @@ extraJavaModuleInfo {
|
||||
requires('com.fasterxml.jackson.core')
|
||||
requires('logback.classic')
|
||||
requires('org.json')
|
||||
requires('io.reactivex.rxjava2')
|
||||
exports('com.samourai.http.client')
|
||||
exports('com.samourai.tor.client')
|
||||
exports('com.samourai.wallet.api.backend')
|
||||
@@ -466,6 +479,17 @@ extraJavaModuleInfo {
|
||||
exports('com.samourai.wallet.client.indexHandler')
|
||||
exports('com.samourai.wallet.hd')
|
||||
exports('com.samourai.wallet.util')
|
||||
exports('com.samourai.wallet.bip47.rpc')
|
||||
exports('com.samourai.wallet.bip47.rpc.java')
|
||||
exports('com.samourai.wallet.cahoots')
|
||||
exports('com.samourai.wallet.cahoots.psbt')
|
||||
exports('com.samourai.wallet.cahoots.stonewallx2')
|
||||
exports('com.samourai.soroban.cahoots')
|
||||
exports('com.samourai.soroban.client')
|
||||
exports('com.samourai.soroban.client.cahoots')
|
||||
exports('com.samourai.soroban.client.meeting')
|
||||
exports('com.samourai.soroban.client.rpc')
|
||||
exports('com.samourai.wallet.send')
|
||||
exports('com.samourai.whirlpool.client.event')
|
||||
exports('com.samourai.whirlpool.client.wallet')
|
||||
exports('com.samourai.whirlpool.client.wallet.beans')
|
||||
@@ -549,7 +573,7 @@ extraJavaModuleInfo {
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
|
||||
module('logback-core-1.2.3.jar', 'logback.core', '1.2.3') {
|
||||
module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
|
||||
requires('java.xml')
|
||||
}
|
||||
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
|
||||
String originalJarName = originalJar.getName();
|
||||
|
||||
//Recreate jackson jars as open, non-synthetic modules
|
||||
if (isModule(originalJar) && !originalJarName.contains("jackson")) {
|
||||
if ((isModule(originalJar) && !originalJarName.contains("jackson")) || originalJarName.startsWith("javafx-")) {
|
||||
outputs.file(originalJar);
|
||||
} else if (moduleInfo.containsKey(originalJarName)) {
|
||||
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
|
||||
|
||||
@@ -51,7 +51,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||
|
||||
The project can cloned for a specific release tag as follows:
|
||||
```shell
|
||||
GIT_TAG="1.5.0"
|
||||
GIT_TAG="1.5.2"
|
||||
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
|
||||
```
|
||||
|
||||
|
||||
+1
-1
Submodule drongo updated: c9e57fad01...ee732fb223
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
cd `dirname $0`
|
||||
args="$*"
|
||||
args="${args%"${args##*[![:space:]]}"}"
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
mime-type=x-scheme-handler/aopp
|
||||
description=Verify Address Ownership URI
|
||||
@@ -6,4 +6,4 @@ Icon=/opt/sparrow/lib/Sparrow.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Unknown
|
||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/aopp
|
||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.5.0</string>
|
||||
<string>1.5.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
@@ -45,14 +45,6 @@
|
||||
<string>bitcoin</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.sparrowwallet.sparrow.aopp</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>aopp</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
|
||||
@@ -77,16 +77,6 @@
|
||||
|
||||
<DirectoryRef Id="TARGETDIR">
|
||||
<Component Id="RegistryEntries" Guid="{206C911C-56EF-44B8-9257-5FD214427965}">
|
||||
<RegistryKey Root="HKCR" Key="aopp" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:Address Ownership Proof Protocol"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="HKCR" Key="bitcoin" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:Bitcoin Payment URL"/>
|
||||
|
||||
@@ -17,6 +17,8 @@ import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
@@ -25,9 +27,14 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.CounterpartyDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.PayNymDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionData;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
@@ -35,6 +42,8 @@ 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;
|
||||
@@ -42,6 +51,7 @@ 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;
|
||||
@@ -52,10 +62,7 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.stage.Window;
|
||||
import javafx.stage.*;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.Notifications;
|
||||
import org.controlsfx.control.StatusBar;
|
||||
@@ -67,6 +74,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;
|
||||
@@ -86,12 +94,18 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem saveTransaction;
|
||||
|
||||
@FXML
|
||||
private MenuItem showTransaction;
|
||||
|
||||
@FXML
|
||||
private Menu savePSBT;
|
||||
|
||||
@FXML
|
||||
private MenuItem savePSBTBinary;
|
||||
|
||||
@FXML
|
||||
private MenuItem showPSBT;
|
||||
|
||||
@FXML
|
||||
private MenuItem exportWallet;
|
||||
|
||||
@@ -118,31 +132,52 @@ 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;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem showUtxosChart;
|
||||
private static final BooleanProperty showLoadingLogProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem showTxHex;
|
||||
private static final BooleanProperty showTxHexProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private MenuItem minimizeToTray;
|
||||
|
||||
@FXML
|
||||
private MenuItem lockWallet;
|
||||
|
||||
@FXML
|
||||
private MenuItem searchWallet;
|
||||
|
||||
@FXML
|
||||
private MenuItem refreshWallet;
|
||||
|
||||
@FXML
|
||||
private MenuItem sendToMany;
|
||||
|
||||
@FXML
|
||||
private MenuItem sweepPrivateKey;
|
||||
|
||||
@FXML
|
||||
private MenuItem findMixingPartner;
|
||||
|
||||
@FXML
|
||||
private MenuItem showPayNym;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem preventSleep;
|
||||
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private StackPane rootStack;
|
||||
|
||||
@@ -159,6 +194,8 @@ public class AppController implements Initializable {
|
||||
|
||||
private Timeline statusTimeline;
|
||||
|
||||
private Tab previouslySelectedTab;
|
||||
|
||||
private final Set<Wallet> loadingWallets = new LinkedHashSet<>();
|
||||
|
||||
private final Set<Wallet> emptyLoadingWallets = new LinkedHashSet<>();
|
||||
@@ -203,7 +240,10 @@ public class AppController implements Initializable {
|
||||
rootStack.getStyleClass().removeAll(DRAG_OVER_CLASS);
|
||||
});
|
||||
|
||||
tabs.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedTab) -> {
|
||||
tabs.getSelectionModel().selectedItemProperty().addListener((observable, previouslySelectedTab, selectedTab) -> {
|
||||
if(tabs.getTabs().contains(previouslySelectedTab)) {
|
||||
this.previouslySelectedTab = previouslySelectedTab;
|
||||
}
|
||||
tabs.getTabs().forEach(tab -> ((Label)tab.getGraphic()).getGraphic().setOpacity(TAB_LABEL_GRAPHIC_OPACITY_INACTIVE));
|
||||
if(selectedTab != null) {
|
||||
Label tabLabel = (Label)selectedTab.getGraphic();
|
||||
@@ -222,6 +262,10 @@ public class AppController implements Initializable {
|
||||
//tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
|
||||
tabs.getTabs().addListener((ListChangeListener<Tab>) c -> {
|
||||
if(c.next() && (c.wasAdded() || c.wasRemoved())) {
|
||||
if(c.wasRemoved() && previouslySelectedTab != null) {
|
||||
tabs.getSelectionModel().select(previouslySelectedTab);
|
||||
}
|
||||
|
||||
boolean walletAdded = c.getAddedSubList().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
|
||||
boolean walletRemoved = c.getRemoved().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
|
||||
if(walletAdded || walletRemoved) {
|
||||
@@ -273,18 +317,36 @@ 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);
|
||||
preventSleepProperty.set(Config.get().isPreventSleep());
|
||||
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
|
||||
|
||||
saveTransaction.setDisable(true);
|
||||
showTransaction.visibleProperty().bind(Bindings.and(saveTransaction.visibleProperty(), saveTransaction.disableProperty().not()));
|
||||
showTransaction.disableProperty().bind(saveTransaction.disableProperty());
|
||||
savePSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
|
||||
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
|
||||
showPSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
|
||||
exportWallet.setDisable(true);
|
||||
lockWallet.setDisable(true);
|
||||
searchWallet.disableProperty().bind(exportWallet.disableProperty());
|
||||
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
|
||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||
sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()));
|
||||
showPayNym.disableProperty().bind(findMixingPartner.disableProperty());
|
||||
findMixingPartner.setDisable(true);
|
||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||
});
|
||||
|
||||
setServerType(Config.get().getServerType());
|
||||
serverToggle.setSelected(isConnected());
|
||||
@@ -360,9 +422,10 @@ public class AppController implements Initializable {
|
||||
Parent root = loader.load();
|
||||
AboutController controller = loader.getController();
|
||||
|
||||
Stage stage = new Stage();
|
||||
Stage stage = new Stage(StageStyle.UNDECORATED);
|
||||
stage.setTitle("About " + MainApp.APP_NAME);
|
||||
stage.initStyle(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.OSX ? StageStyle.UNDECORATED : StageStyle.DECORATED);
|
||||
stage.initOwner(tabs.getScene().getWindow());
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.setResizable(false);
|
||||
Scene scene = new Scene(root);
|
||||
AppServices.onEscapePressed(scene, stage::close);
|
||||
@@ -370,6 +433,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) {
|
||||
@@ -429,10 +495,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 {
|
||||
@@ -561,6 +624,23 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void showTransaction(ActionEvent event) {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData) tabData;
|
||||
Transaction transaction = transactionTabData.getTransaction();
|
||||
|
||||
try {
|
||||
UR ur = UR.fromBytes(transaction.bitcoinSerialize());
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur);
|
||||
qrDisplayDialog.showAndWait();
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating UR", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void savePSBTBinary(ActionEvent event) {
|
||||
savePSBT(false, true);
|
||||
}
|
||||
@@ -644,6 +724,18 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void showPSBT(ActionEvent event) {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
|
||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(transactionTabData.getPsbt().serialize());
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR());
|
||||
qrDisplayDialog.show();
|
||||
}
|
||||
}
|
||||
|
||||
public List<WalletTabData> getOpenWalletTabData() {
|
||||
List<WalletTabData> openWalletTabData = new ArrayList<>();
|
||||
|
||||
@@ -719,12 +811,6 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new LoadingLogChangedEvent(item.isSelected()));
|
||||
}
|
||||
|
||||
public void showUtxosChart(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setShowUtxosChart(item.isSelected());
|
||||
EventManager.get().post(new UtxosChartChangedEvent(item.isSelected()));
|
||||
}
|
||||
|
||||
public void showTxHex(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setShowTransactionHex(item.isSelected());
|
||||
@@ -738,6 +824,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);
|
||||
@@ -794,8 +886,21 @@ public class AppController implements Initializable {
|
||||
try {
|
||||
Storage storage = new Storage(file);
|
||||
if(!storage.isEncrypted()) {
|
||||
WalletBackupAndKey walletBackupAndKey = storage.loadUnencryptedWallet();
|
||||
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
|
||||
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage);
|
||||
loadWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
WalletBackupAndKey walletBackupAndKey = loadWalletService.getValue();
|
||||
openWallet(storage, walletBackupAndKey, this, forceSameWindow);
|
||||
});
|
||||
loadWalletService.setOnFailed(workerStateEvent -> {
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
if(exception instanceof StorageException) {
|
||||
showErrorDialog("Error Opening Wallet", exception.getMessage());
|
||||
} else if(!attemptImportWallet(file, null)) {
|
||||
log.error("Error opening wallet", exception);
|
||||
showErrorDialog("Error Opening Wallet", exception.getMessage() == null || exception.getMessage().contains("Expected BEGIN_OBJECT") ? "Unsupported wallet file format." : exception.getMessage());
|
||||
}
|
||||
});
|
||||
loadWalletService.start();
|
||||
} else {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(storage.getWalletName(null), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> optionalPassword = dlg.showAndWait();
|
||||
@@ -819,9 +924,11 @@ public class AppController implements Initializable {
|
||||
Platform.runLater(() -> openWalletFile(file, forceSameWindow));
|
||||
}
|
||||
} else {
|
||||
if(!attemptImportWallet(file, password)) {
|
||||
if(exception instanceof StorageException) {
|
||||
showErrorDialog("Error Opening Wallet", exception.getMessage());
|
||||
} else if(!attemptImportWallet(file, password)) {
|
||||
log.error("Error Opening Wallet", exception);
|
||||
showErrorDialog("Error Opening Wallet", exception.getMessage() == null ? "Unsupported file format" : exception.getMessage());
|
||||
showErrorDialog("Error Opening Wallet", exception.getMessage() == null || exception.getMessage().contains("Expected BEGIN_OBJECT") ? "Unsupported wallet file format." : exception.getMessage());
|
||||
}
|
||||
password.clear();
|
||||
}
|
||||
@@ -829,8 +936,6 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(null), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
loadWalletService.start();
|
||||
}
|
||||
} catch(StorageException e) {
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||
} catch(Exception e) {
|
||||
if(e instanceof IOException && e.getMessage().startsWith("The process cannot access the file because another process has locked")) {
|
||||
log.error("Error opening wallet", e);
|
||||
@@ -879,7 +984,7 @@ public class AppController implements Initializable {
|
||||
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 {
|
||||
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getFullName(), copyKeystore);
|
||||
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getFullDisplayName(), copyKeystore);
|
||||
Optional<String> optionalPassphrase = passphraseDialog.showAndWait();
|
||||
if(optionalPassphrase.isPresent()) {
|
||||
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
|
||||
@@ -906,6 +1011,17 @@ public class AppController implements Initializable {
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
|
||||
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
soroban.setHDWallet(copy);
|
||||
} else if(Config.get().isUsePayNym() && SorobanServices.canWalletMix(wallet)) {
|
||||
String walletId = storage.getWalletId(wallet);
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
soroban.setPaymentCode(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++) {
|
||||
@@ -941,6 +1057,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(),
|
||||
@@ -1136,10 +1253,132 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void sweepPrivateKey(ActionEvent event) {
|
||||
Wallet wallet = null;
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
wallet = selectedWalletForm.getWallet();
|
||||
}
|
||||
|
||||
PrivateKeySweepDialog dialog = new PrivateKeySweepDialog(wallet);
|
||||
Optional<Transaction> optTransaction = dialog.showAndWait();
|
||||
optTransaction.ifPresent(transaction -> addTransactionTab(null, null, transaction));
|
||||
}
|
||||
|
||||
public void findMixingPartner(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(selectedWalletForm.getWalletId());
|
||||
if(soroban.getHdWallet() == null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage storage = selectedWalletForm.getStorage();
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
copy.decrypt(key);
|
||||
|
||||
try {
|
||||
soroban.setHDWallet(copy);
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
} finally {
|
||||
key.clear();
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(() -> findMixingPartner(null));
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
soroban.setHDWallet(wallet);
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
}
|
||||
} else {
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showPayNym(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
PayNymDialog payNymDialog = new PayNymDialog(selectedWalletForm.getWalletId(), false);
|
||||
payNymDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
public void minimizeToTray(ActionEvent event) {
|
||||
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 lockWallets(ActionEvent event) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData instanceof WalletTabData walletTabData) {
|
||||
if(!walletTabData.getWalletForm().isLocked()) {
|
||||
EventManager.get().post(new WalletLockEvent(walletTabData.getWalletForm().getMasterWallet()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void searchWallet(ActionEvent event) {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
if(selectedTab != null) {
|
||||
TabData tabData = (TabData) selectedTab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane) selectedTab.getContent();
|
||||
List<WalletForm> walletForms = subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm()).collect(Collectors.toList());
|
||||
if(!walletForms.isEmpty()) {
|
||||
SearchWalletDialog searchWalletDialog = new SearchWalletDialog(walletForms);
|
||||
Optional<Entry> optEntry = searchWalletDialog.showAndWait();
|
||||
if(optEntry.isPresent()) {
|
||||
Entry entry = optEntry.get();
|
||||
EventManager.get().post(new FunctionActionEvent(entry.getWalletFunction(), entry.getWallet()));
|
||||
Platform.runLater(() -> EventManager.get().post(new SelectEntryEvent(entry)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshWallet(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
@@ -1189,7 +1428,6 @@ 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) {
|
||||
@@ -1202,13 +1440,17 @@ public class AppController implements Initializable {
|
||||
|
||||
TabPane subTabs = new TabPane();
|
||||
subTabs.setSide(Side.RIGHT);
|
||||
subTabs.getStyleClass().add("master-only");
|
||||
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) {
|
||||
@@ -1232,12 +1474,11 @@ public class AppController implements Initializable {
|
||||
if(walletTabData.getWallet() == wallet.getMasterWallet()) {
|
||||
TabPane subTabs = (TabPane)walletTab.getContent();
|
||||
addWalletSubTab(subTabs, storage, wallet, backupWallet);
|
||||
Tab masterTab = subTabs.getTabs().get(0);
|
||||
Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0));
|
||||
Label masterLabel = (Label)masterTab.getGraphic();
|
||||
masterLabel.setText(getAutomaticName(wallet.getMasterWallet()));
|
||||
masterLabel.setText(wallet.getMasterWallet().getLabel() != null ? wallet.getMasterWallet().getLabel() : wallet.getMasterWallet().getAutomaticName());
|
||||
Platform.runLater(() -> {
|
||||
subTabs.getStyleClass().remove("master-only");
|
||||
subTabs.getStyleClass().add("wallet-subtabs");
|
||||
setSubTabsVisible(subTabs, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1247,11 +1488,27 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new WalletOpenedEvent(storage, wallet));
|
||||
}
|
||||
|
||||
private void setSubTabsVisible(TabPane subTabs, boolean visible) {
|
||||
if(visible) {
|
||||
subTabs.getStyleClass().remove("master-only");
|
||||
if(!subTabs.getStyleClass().contains("wallet-subtabs")) {
|
||||
subTabs.getStyleClass().add("wallet-subtabs");
|
||||
}
|
||||
} else {
|
||||
if(!subTabs.getStyleClass().contains("master-only")) {
|
||||
subTabs.getStyleClass().add("master-only");
|
||||
}
|
||||
subTabs.getStyleClass().remove("wallet-subtabs");
|
||||
}
|
||||
}
|
||||
|
||||
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet, Wallet backupWallet) {
|
||||
try {
|
||||
Tab subTab = new Tab();
|
||||
subTab.setClosable(false);
|
||||
Label subTabLabel = new Label(wallet.isMasterWallet() ? getAutomaticName(wallet) : wallet.getName());
|
||||
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);
|
||||
@@ -1269,8 +1526,16 @@ public class AppController implements Initializable {
|
||||
|
||||
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
|
||||
subTab.setUserData(tabData);
|
||||
if(!wallet.isWhirlpoolChildWallet()) {
|
||||
subTab.setContextMenu(getSubTabContextMenu(wallet, subTabs, subTab));
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1296,42 +1561,21 @@ public class AppController implements Initializable {
|
||||
return tabGlyph;
|
||||
}
|
||||
|
||||
private String getAutomaticName(Wallet wallet) {
|
||||
int account = wallet.getAccountIndex();
|
||||
return account < 0 ? wallet.getName() : (!wallet.isWhirlpoolMasterWallet() || account > 1 ? "Account #" + account : "Deposit");
|
||||
}
|
||||
|
||||
public WalletForm getSelectedWalletForm() {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)selectedTab.getContent();
|
||||
Tab selectedSubTab = subTabs.getSelectionModel().getSelectedItem();
|
||||
WalletTabData subWalletTabData = (WalletTabData)selectedSubTab.getUserData();
|
||||
return subWalletTabData.getWalletForm();
|
||||
if(selectedTab != null) {
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)selectedTab.getContent();
|
||||
Tab selectedSubTab = subTabs.getSelectionModel().getSelectedItem();
|
||||
WalletTabData subWalletTabData = (WalletTabData)selectedSubTab.getUserData();
|
||||
return subWalletTabData.getWalletForm();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void openExamples(ActionEvent event) {
|
||||
try {
|
||||
addTransactionTab("p2pkh", null, "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac06241559");
|
||||
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 void addTransactionTab(String name, File file, String string) throws ParseException, PSBTParseException, TransactionParseException {
|
||||
if(Utils.isBase64(string) && !Utils.isHex(string)) {
|
||||
addTransactionTab(name, file, Base64.getDecoder().decode(string));
|
||||
@@ -1480,6 +1724,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);
|
||||
@@ -1501,6 +1757,41 @@ public class AppController implements Initializable {
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
private ContextMenu getSubTabContextMenu(Wallet wallet, TabPane subTabs, 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);
|
||||
|
||||
wallet.setLabel(label);
|
||||
EventManager.get().post(new WalletLabelChangedEvent(wallet));
|
||||
}
|
||||
});
|
||||
contextMenu.getItems().add(rename);
|
||||
|
||||
if(!wallet.isMasterWallet() && !wallet.isWhirlpoolChildWallet()) {
|
||||
MenuItem delete = new MenuItem("Delete Account");
|
||||
delete.setOnAction(event -> {
|
||||
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Delete Wallet Account?", "Labels applied in this wallet account will be lost. Are you sure?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.OK) {
|
||||
subTabs.getTabs().remove(subTab);
|
||||
if(subTabs.getTabs().size() == 1) {
|
||||
setSubTabsVisible(subTabs, false);
|
||||
}
|
||||
EventManager.get().post(new WalletDeletedEvent(wallet));
|
||||
}
|
||||
});
|
||||
contextMenu.getItems().add(delete);
|
||||
}
|
||||
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
public void setServerType(ServerType serverType) {
|
||||
if(serverType == ServerType.PUBLIC_ELECTRUM_SERVER && !serverToggle.getStyleClass().contains("public-server")) {
|
||||
serverToggle.getStyleClass().add("public-server");
|
||||
@@ -1529,21 +1820,17 @@ 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.setFromValue(1.0);
|
||||
fadeTransition.setToValue(0.4);
|
||||
fadeTransition.setAutoReverse(true);
|
||||
fadeTransition.setCycleCount(Animation.INDEFINITE);
|
||||
fadeTransition.play();
|
||||
serverToggle.setUserData(fadeTransition);
|
||||
Timeline timeline = AnimationUtil.getPulse(thumbArea, Duration.millis(600), 1.0, 0.4, 8);
|
||||
timeline.play();
|
||||
serverToggle.setUserData(new AnimationUtil.AnimatedNode(thumbArea, timeline));
|
||||
}
|
||||
}
|
||||
|
||||
private void serverToggleStopAnimation() {
|
||||
if(serverToggle.getUserData() != null) {
|
||||
FadeTransition fadeTransition = (FadeTransition)serverToggle.getUserData();
|
||||
fadeTransition.stop();
|
||||
fadeTransition.getNode().setOpacity(1.0);
|
||||
AnimationUtil.AnimatedNode animatedNode = (AnimationUtil.AnimatedNode)serverToggle.getUserData();
|
||||
animatedNode.timeline().stop();
|
||||
animatedNode.node().setOpacity(1.0);
|
||||
serverToggle.setUserData(null);
|
||||
}
|
||||
}
|
||||
@@ -1559,13 +1846,9 @@ public class AppController implements Initializable {
|
||||
private void tabLabelStartAnimation(Tab tab) {
|
||||
Label tabLabel = (Label) tab.getGraphic();
|
||||
if(tabLabel.getUserData() == null) {
|
||||
FadeTransition fadeTransition = new FadeTransition(Duration.millis(1000), tabLabel.getGraphic());
|
||||
fadeTransition.setFromValue(tabLabel.getGraphic().getOpacity());
|
||||
fadeTransition.setToValue(0.1);
|
||||
fadeTransition.setAutoReverse(true);
|
||||
fadeTransition.setCycleCount(Animation.INDEFINITE);
|
||||
fadeTransition.play();
|
||||
tabLabel.setUserData(fadeTransition);
|
||||
Timeline timeline = AnimationUtil.getPulse(tabLabel.getGraphic(), Duration.millis(1000), tabLabel.getGraphic().getOpacity(), 0.1, 8);
|
||||
timeline.play();
|
||||
tabLabel.setUserData(timeline);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1595,8 +1878,8 @@ public class AppController implements Initializable {
|
||||
private void tabLabelStopAnimation(Tab tab) {
|
||||
Label tabLabel = (Label) tab.getGraphic();
|
||||
if(tabLabel.getUserData() != null) {
|
||||
FadeTransition fadeTransition = (FadeTransition)tabLabel.getUserData();
|
||||
fadeTransition.stop();
|
||||
Animation animation = (Animation)tabLabel.getUserData();
|
||||
animation.stop();
|
||||
tabLabel.setUserData(null);
|
||||
tabLabel.getGraphic().setOpacity(tab.isSelected() ? TAB_LABEL_GRAPHIC_OPACITY_ACTIVE : TAB_LABEL_GRAPHIC_OPACITY_INACTIVE);
|
||||
}
|
||||
@@ -1665,19 +1948,21 @@ public class AppController implements Initializable {
|
||||
} else {
|
||||
saveTransaction.setVisible(false);
|
||||
}
|
||||
lockWallet.setDisable(true);
|
||||
exportWallet.setDisable(true);
|
||||
showLoadingLog.setDisable(true);
|
||||
showUtxosChart.setDisable(true);
|
||||
showTxHex.setDisable(false);
|
||||
findMixingPartner.setDisable(true);
|
||||
} else if(event instanceof WalletTabSelectedEvent) {
|
||||
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
|
||||
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(true);
|
||||
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
|
||||
lockWallet.setDisable(walletTabData.getWalletForm().lockedProperty().get());
|
||||
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid() || walletTabData.getWalletForm().isLocked());
|
||||
showLoadingLog.setDisable(false);
|
||||
showUtxosChart.setDisable(false);
|
||||
showTxHex.setDisable(true);
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1701,7 +1986,8 @@ public class AppController implements Initializable {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
||||
exportWallet.setDisable(!event.getWallet().isValid());
|
||||
exportWallet.setDisable(!event.getWallet().isValid() || selectedWalletForm.isLocked());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1709,8 +1995,22 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void newWalletTransactions(NewWalletTransactionsEvent event) {
|
||||
if(Config.get().isNotifyNewTransactions() && getOpenWallets().containsKey(event.getWallet())) {
|
||||
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();
|
||||
List<BlockTransaction> whirlpoolTransactions = event.getUnspentConfirmingWhirlpoolMixTransactions();
|
||||
blockTransactions.removeAll(whirlpoolTransactions);
|
||||
|
||||
if(!whirlpoolTransactions.isEmpty()) {
|
||||
@@ -1817,8 +2117,15 @@ public class AppController implements Initializable {
|
||||
AppServices.get().getApplication().getHostServices().showDocument("https://www.sparrowwallet.com/download");
|
||||
});
|
||||
|
||||
if(statusBar.getRightItems().size() > 0 && statusBar.getRightItems().get(0) instanceof Hyperlink) {
|
||||
statusBar.getRightItems().remove(0);
|
||||
Hyperlink existingUpdateLabel = null;
|
||||
for(Node node : statusBar.getRightItems()) {
|
||||
if(node instanceof Hyperlink) {
|
||||
existingUpdateLabel = (Hyperlink)node;
|
||||
}
|
||||
}
|
||||
|
||||
if(existingUpdateLabel != null) {
|
||||
statusBar.getRightItems().remove(existingUpdateLabel);
|
||||
}
|
||||
|
||||
statusBar.getRightItems().add(0, versionUpdateLabel);
|
||||
@@ -1939,7 +2246,7 @@ 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);
|
||||
@@ -2149,12 +2456,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void sendAction(SendActionEvent event) {
|
||||
selectTab(event.getWallet());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void recieveAction(ReceiveActionEvent event) {
|
||||
public void functionAction(FunctionActionEvent event) {
|
||||
selectTab(event.getWallet());
|
||||
}
|
||||
|
||||
@@ -2167,4 +2469,22 @@ public class AppController implements Initializable {
|
||||
|
||||
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);
|
||||
exportWallet.setDisable(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletUnlock(WalletUnlockEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
|
||||
lockWallet.setDisable(false);
|
||||
exportWallet.setDisable(!event.getWallet().isValid());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||
@@ -10,13 +9,16 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
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.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@@ -24,6 +26,7 @@ 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;
|
||||
@@ -48,6 +51,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.*;
|
||||
@@ -72,10 +76,16 @@ public class AppServices {
|
||||
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
|
||||
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
||||
|
||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
||||
public static final List<Long> FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L);
|
||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||
|
||||
private static AppServices INSTANCE;
|
||||
|
||||
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
|
||||
|
||||
private final SorobanServices sorobanServices = new SorobanServices();
|
||||
|
||||
private final MainApp application;
|
||||
|
||||
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
|
||||
@@ -94,13 +104,15 @@ 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<>();
|
||||
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
||||
|
||||
private static Double minimumRelayFeeRate;
|
||||
|
||||
@@ -148,6 +160,7 @@ public class AppServices {
|
||||
this.application = application;
|
||||
EventManager.get().register(this);
|
||||
EventManager.get().register(whirlpoolServices);
|
||||
EventManager.get().register(sorobanServices);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -156,6 +169,7 @@ public class AppServices {
|
||||
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
|
||||
versionCheckService = createVersionCheckService();
|
||||
torService = createTorService();
|
||||
preventSleepService = createPreventSleepService();
|
||||
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
|
||||
@@ -183,6 +197,10 @@ public class AppServices {
|
||||
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET) {
|
||||
restartService(versionCheckService);
|
||||
}
|
||||
|
||||
if(config.isPreventSleep()) {
|
||||
restartService(preventSleepService);
|
||||
}
|
||||
}
|
||||
|
||||
private void restartService(ScheduledService<?> service) {
|
||||
@@ -382,6 +400,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;
|
||||
}
|
||||
@@ -430,6 +481,10 @@ public class AppServices {
|
||||
return get().whirlpoolServices;
|
||||
}
|
||||
|
||||
public static SorobanServices getSorobanServices() {
|
||||
return get().sorobanServices;
|
||||
}
|
||||
|
||||
public static AppController newAppWindow(Stage stage) {
|
||||
try {
|
||||
FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml"));
|
||||
@@ -441,7 +496,7 @@ public class AppServices {
|
||||
|
||||
stage.setTitle("Sparrow");
|
||||
stage.setMinWidth(650);
|
||||
stage.setMinHeight(730);
|
||||
stage.setMinHeight(708);
|
||||
stage.setScene(scene);
|
||||
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow-large.png")));
|
||||
|
||||
@@ -494,6 +549,18 @@ public class AppServices {
|
||||
return getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public WalletTransaction getCreatedTransaction(Set<BlockTransactionHashIndex> utxos) {
|
||||
for(List<WalletTabData> walletTabDataList : walletWindows.values()) {
|
||||
for(WalletTabData walletTabData : walletTabDataList) {
|
||||
if(walletTabData.getWalletForm().getCreatedWalletTransaction() != null && utxos.equals(walletTabData.getWalletForm().getCreatedWalletTransaction().getSelectedUtxos().keySet())) {
|
||||
return walletTabData.getWalletForm().getCreatedWalletTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 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);
|
||||
@@ -528,11 +595,16 @@ public class AppServices {
|
||||
return latestBlockHeader;
|
||||
}
|
||||
|
||||
public static Double getDefaultFeeRate() {
|
||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||
return getTargetBlockFeeRates() == null ? FALLBACK_FEE_RATE : getTargetBlockFeeRates().get(defaultTarget);
|
||||
}
|
||||
|
||||
public static Map<Integer, Double> getTargetBlockFeeRates() {
|
||||
return targetBlockFeeRates;
|
||||
}
|
||||
|
||||
public static Map<Date, Set<MempoolRateSize>> getMempoolHistogram() {
|
||||
public static TreeMap<Date, Set<MempoolRateSize>> getMempoolHistogram() {
|
||||
return mempoolHistogram;
|
||||
}
|
||||
|
||||
@@ -543,6 +615,9 @@ public class AppServices {
|
||||
}
|
||||
|
||||
mempoolHistogram.put(Date.from(dateMinute.atZone(ZoneId.systemDefault()).toInstant()), rateSizes);
|
||||
|
||||
Date yesterday = Date.from(LocalDateTime.now().minusDays(1).atZone(ZoneId.systemDefault()).toInstant());
|
||||
mempoolHistogram.keySet().removeIf(date -> date.before(yesterday));
|
||||
}
|
||||
|
||||
public static Double getMinimumRelayFeeRate() {
|
||||
@@ -581,7 +656,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) {
|
||||
@@ -654,7 +729,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);
|
||||
@@ -717,8 +794,6 @@ public class AppServices {
|
||||
Platform.runLater(() -> {
|
||||
if("bitcoin".equals(uri.getScheme())) {
|
||||
openBitcoinUri(uri);
|
||||
} else if("aopp".equals(uri.getScheme())) {
|
||||
openAddressOwnershipProof(uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -753,20 +828,6 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
public static void openAddressOwnershipProof(URI uri) {
|
||||
try {
|
||||
Aopp aopp = new Aopp(uri);
|
||||
Wallet wallet = selectWallet(aopp.getScriptType(), "send proof of address");
|
||||
|
||||
if(wallet != null) {
|
||||
EventManager.get().post(new ReceiveActionEvent(wallet));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveProofEvent(wallet, aopp)));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
showErrorDialog("Not a valid AOPP URI", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Wallet selectWallet(ScriptType scriptType, String actionDescription) {
|
||||
Wallet wallet = null;
|
||||
List<Wallet> wallets = get().getOpenWallets().keySet().stream().filter(w -> scriptType == null || w.getScriptType() == scriptType).collect(Collectors.toList());
|
||||
@@ -803,6 +864,7 @@ public class AppServices {
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
Config.get().addRecentServer();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -8,7 +8,6 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.Bwt;
|
||||
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
@@ -31,7 +30,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.5.0";
|
||||
public static final String APP_VERSION = "1.5.6";
|
||||
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";
|
||||
@@ -70,7 +69,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);
|
||||
@@ -78,16 +76,14 @@ public class MainApp extends Application {
|
||||
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
|
||||
} else if(Network.get() == Network.MAINNET) {
|
||||
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
|
||||
Config.get().setPublicElectrumServer(PublicElectrumServer.values()[new Random().nextInt(PublicElectrumServer.values().length)].getUrl());
|
||||
List<PublicElectrumServer> servers = PublicElectrumServer.getServers();
|
||||
Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
}
|
||||
|
||||
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
|
||||
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (duplicate ? " (Duplicate address)" : "");
|
||||
return utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : "");
|
||||
}
|
||||
|
||||
public static Glyph getDuplicateGlyph() {
|
||||
|
||||
@@ -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,44 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.scene.Node;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class AnimationUtil {
|
||||
public static Timeline getSlowFadeOut(Node node, Duration duration, double fromValue, int numIncrements) {
|
||||
Timeline fadeTimeline = new Timeline();
|
||||
Duration incrementDuration = duration.divide(numIncrements);
|
||||
for(int i = 0; i < numIncrements; i++) {
|
||||
double percent = ((double)numIncrements - i - 1) / numIncrements;
|
||||
double opacity = percent * fromValue;
|
||||
fadeTimeline.getKeyFrames().add(new KeyFrame(incrementDuration.multiply(i+1), event -> node.setOpacity(opacity)));
|
||||
}
|
||||
|
||||
return fadeTimeline;
|
||||
}
|
||||
|
||||
public static Timeline getPulse(Node node, Duration duration, double fromValue, double toValue, int numIncrements) {
|
||||
Timeline pulseTimeline = getFade(node, duration, fromValue, toValue, numIncrements);
|
||||
|
||||
pulseTimeline.setCycleCount(Animation.INDEFINITE);
|
||||
pulseTimeline.setAutoReverse(true);
|
||||
|
||||
return pulseTimeline;
|
||||
}
|
||||
|
||||
public static Timeline getFade(Node node, Duration duration, double fromValue, double toValue, int numIncrements) {
|
||||
Timeline fadeTimeline = new Timeline();
|
||||
Duration incrementDuration = duration.divide(numIncrements);
|
||||
for(int i = 0; i < numIncrements; i++) {
|
||||
double percent = ((double) numIncrements - i - 1) / numIncrements; //From 99% to 0%
|
||||
double opacity = (percent * (fromValue - toValue)) + toValue;
|
||||
fadeTimeline.getKeyFrames().add(new KeyFrame(incrementDuration.multiply(i+1), event -> node.setOpacity(opacity)));
|
||||
}
|
||||
|
||||
return fadeTimeline;
|
||||
}
|
||||
|
||||
public record AnimatedNode (Node node, Timeline timeline) {}
|
||||
}
|
||||
@@ -1,20 +1,26 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class BalanceChart extends LineChart<Number, Number> {
|
||||
private static final int MAX_VALUES = 500;
|
||||
|
||||
private XYChart.Series<Number, Number> balanceSeries;
|
||||
|
||||
private TransactionEntry selectedEntry;
|
||||
@@ -37,7 +43,7 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||
setVisible(!walletTransactionsEntry.getChildren().isEmpty());
|
||||
balanceSeries.getData().clear();
|
||||
|
||||
List<Data<Number, Number>> balanceDataList = walletTransactionsEntry.getChildren().stream()
|
||||
List<Data<Number, Number>> balanceDataList = getTransactionEntries(walletTransactionsEntry)
|
||||
.map(entry -> (TransactionEntry)entry)
|
||||
.filter(txEntry -> txEntry.getBlockTransaction().getHeight() > 0)
|
||||
.map(txEntry -> new XYChart.Data<>((Number)txEntry.getBlockTransaction().getDate().getTime(), (Number)txEntry.getBalance(), txEntry))
|
||||
@@ -74,6 +80,24 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<Entry> getTransactionEntries(WalletTransactionsEntry walletTransactionsEntry) {
|
||||
int total = walletTransactionsEntry.getChildren().size();
|
||||
if(walletTransactionsEntry.getChildren().size() <= MAX_VALUES) {
|
||||
return walletTransactionsEntry.getChildren().stream();
|
||||
}
|
||||
|
||||
int bucketSize = total / MAX_VALUES;
|
||||
List<List<Entry>> buckets = Lists.partition(walletTransactionsEntry.getChildren(), bucketSize);
|
||||
List<Entry> reducedEntries = new ArrayList<>(MAX_VALUES);
|
||||
for(List<Entry> bucket : buckets) {
|
||||
long max = bucket.stream().mapToLong(entry -> Math.abs(entry.getValue())).max().orElse(0);
|
||||
Entry bucketEntry = bucket.stream().filter(entry -> entry.getValue() == max || entry.getValue() == -max).findFirst().orElseThrow();
|
||||
reducedEntries.add(bucketEntry);
|
||||
}
|
||||
|
||||
return reducedEntries.stream();
|
||||
}
|
||||
|
||||
public void select(TransactionEntry transactionEntry) {
|
||||
Set<Node> selectedSymbols = lookupAll(".chart-line-symbol.selected");
|
||||
for(Node selectedSymbol : selectedSymbols) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.Axis;
|
||||
@@ -28,7 +28,7 @@ public class BlockTargetFeeRatesChart extends LineChart<String, Number> {
|
||||
|
||||
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
|
||||
Integer targetBlocks = targetBlocksIter.next();
|
||||
if(SendController.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
|
||||
if(AppServices.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
|
||||
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
|
||||
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
|
||||
feeRateSeries.getData().add(data);
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
@@ -15,7 +16,7 @@ import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
public class CoinLabel extends CopyableLabel {
|
||||
public class CoinLabel extends Label {
|
||||
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
@@ -28,7 +29,6 @@ public class CoinLabel extends CopyableLabel {
|
||||
|
||||
public CoinLabel(String text) {
|
||||
super(text);
|
||||
BTC_FORMAT.setMaximumFractionDigits(8);
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new CoinContextMenu();
|
||||
@@ -77,7 +77,7 @@ public class CoinLabel extends CopyableLabel {
|
||||
|
||||
private class CoinContextMenu extends ContextMenu {
|
||||
public CoinContextMenu() {
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in Satoshis");
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||
copySatsValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.application.Platform;
|
||||
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<>();
|
||||
|
||||
private boolean initialized;
|
||||
private boolean comboShowing;
|
||||
|
||||
public ComboBoxTextField() {
|
||||
super();
|
||||
getStyleClass().add("combo-text-field");
|
||||
setupComboButtonField(super.rightProperty());
|
||||
|
||||
disabledProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(comboProperty.isNotNull().get()) {
|
||||
comboProperty.get().setVisible(!newValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupComboButtonField(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()) {
|
||||
if(comboShowing) {
|
||||
comboProperty.get().hide();
|
||||
} else {
|
||||
comboProperty.get().show();
|
||||
}
|
||||
|
||||
comboShowing = !comboShowing;
|
||||
|
||||
if(!initialized) {
|
||||
comboProperty.get().valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
comboShowing = false;
|
||||
Platform.runLater(() -> comboProperty.get().getSelectionModel().clearSelection());
|
||||
});
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
rightProperty.set(showComboButtonPane);
|
||||
}
|
||||
|
||||
public ComboBox<?> getComboProperty() {
|
||||
return comboProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<ComboBox<?>> walletComboProperty() {
|
||||
return comboProperty;
|
||||
}
|
||||
|
||||
public void setComboProperty(ComboBox<?> comboProperty) {
|
||||
this.comboProperty.set(comboProperty);
|
||||
}
|
||||
}
|
||||
@@ -86,10 +86,10 @@ public class ConfirmationProgressIndicator extends StackPane {
|
||||
upTickLineTimeline.getKeyFrames().add(upTickLineFrame);
|
||||
sequence.getChildren().add(upTickLineTimeline);
|
||||
|
||||
FadeTransition groupFadeOut = new FadeTransition(Duration.minutes(10), confirmationGroup);
|
||||
groupFadeOut.setFromValue(1);
|
||||
groupFadeOut.setToValue(0);
|
||||
Timeline groupFadeOut = AnimationUtil.getSlowFadeOut(confirmationGroup, Duration.minutes(10), 1.0, 10);
|
||||
sequence.getChildren().add(groupFadeOut);
|
||||
|
||||
confirmationsProperty().unbind();
|
||||
}
|
||||
|
||||
sequence.play();
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class CopyableCoinLabel extends CopyableLabel {
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
private final CoinContextMenu contextMenu;
|
||||
|
||||
public CopyableCoinLabel() {
|
||||
this("Unknown");
|
||||
}
|
||||
|
||||
public CopyableCoinLabel(String text) {
|
||||
super(text);
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new CoinContextMenu();
|
||||
}
|
||||
|
||||
public final LongProperty valueProperty() {
|
||||
return valueProperty;
|
||||
}
|
||||
|
||||
public final long getValue() {
|
||||
return valueProperty.get();
|
||||
}
|
||||
|
||||
public final void setValue(long value) {
|
||||
this.valueProperty.set(value);
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
refresh(Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void refresh(BitcoinUnit bitcoinUnit) {
|
||||
setValueAsText(getValue(), bitcoinUnit);
|
||||
}
|
||||
|
||||
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
|
||||
String btcValue = CoinLabel.getBTCFormat().format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
|
||||
|
||||
BitcoinUnit unit = bitcoinUnit;
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
|
||||
}
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setText(satsValue);
|
||||
setText(btcValue);
|
||||
} else {
|
||||
tooltip.setText(btcValue);
|
||||
setText(satsValue);
|
||||
}
|
||||
}
|
||||
|
||||
private class CoinContextMenu extends ContextMenu {
|
||||
public CoinContextMenu() {
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||
copySatsValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(Long.toString(getValue()));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
MenuItem copyBtcValue = new MenuItem("Copy Value in BTC");
|
||||
copyBtcValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(CoinLabel.getBTCFormat().format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(copySatsValue, copyBtcValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.util.Duration;
|
||||
@@ -16,10 +22,36 @@ import org.controlsfx.control.textfield.CustomTextField;
|
||||
public class CopyableTextField extends CustomTextField {
|
||||
private static final Duration FADE_DURATION = Duration.millis(350);
|
||||
|
||||
private final ChangeListener<String> selectionListener = (textObservable, textOldValue, textNewValue) -> {
|
||||
if(!textNewValue.isEmpty()) {
|
||||
deselect();
|
||||
}
|
||||
};
|
||||
|
||||
private final EventHandler<MouseEvent> copyHandler = event -> {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(getCopyText());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
|
||||
Tooltip tooltip = new Tooltip("Copied!");
|
||||
tooltip.show(this, event.getScreenX(), event.getScreenY());
|
||||
Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(1), e -> tooltip.hide()));
|
||||
timeline.play();
|
||||
};
|
||||
|
||||
public CopyableTextField() {
|
||||
super();
|
||||
getStyleClass().add("copyable-text-field");
|
||||
setupCopyButtonField(super.rightProperty());
|
||||
editableProperty().addListener((observable, oldValue, editable) -> {
|
||||
if(!editable) {
|
||||
setOnMouseClicked(copyHandler);
|
||||
selectedTextProperty().addListener(selectionListener);
|
||||
} else {
|
||||
setOnMouseClicked(null);
|
||||
selectedTextProperty().removeListener(selectionListener);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
|
||||
@@ -31,7 +63,7 @@ public class CopyableTextField extends CustomTextField {
|
||||
copyButtonPane.setCursor(Cursor.DEFAULT);
|
||||
copyButtonPane.setOnMouseReleased(e -> {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(getText());
|
||||
content.putString(getCopyText());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
@@ -61,4 +93,8 @@ public class CopyableTextField extends CustomTextField {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected String getCopyText() {
|
||||
return getText();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,14 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
if(utxoEntry.getHashIndex().getHeight() <= 0) {
|
||||
setText("Unconfirmed " + (utxoEntry.getHashIndex().getHeight() < 0 ? "Parent " : "") + (utxoEntry.getWallet().isWhirlpoolMixWallet() ? "(Not yet mixable)" : (utxoEntry.isSpendable() ? "(Spendable)" : "(Not yet spendable)")));
|
||||
} else {
|
||||
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;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
@@ -79,7 +80,7 @@ public class DescriptorArea extends CodeArea {
|
||||
copyOutputDescriptor.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(OutputDescriptor.getOutputDescriptor(wallet).toString(true));
|
||||
content.putString(OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null).toString(true));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyOutputDescriptor);
|
||||
|
||||
@@ -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();
|
||||
@@ -287,7 +332,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setMaxHeight(120);
|
||||
vBox.setSpacing(42);
|
||||
pinField = (CustomPasswordField)TextFields.createClearablePasswordField();
|
||||
pinField = new ViewPasswordField();
|
||||
Platform.runLater(() -> pinField.requestFocus());
|
||||
enterPinButton = new Button("Enter PIN");
|
||||
enterPinButton.setDefaultButton(true);
|
||||
@@ -327,7 +372,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private Node getPassphraseEntry() {
|
||||
CustomPasswordField passphraseField = (CustomPasswordField)TextFields.createClearablePasswordField();
|
||||
CustomPasswordField passphraseField = new ViewPasswordField();
|
||||
passphrase.bind(passphraseField.textProperty());
|
||||
HBox.setHgrow(passphraseField, Priority.ALWAYS);
|
||||
passphraseField.setOnAction(event -> {
|
||||
@@ -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);
|
||||
@@ -578,7 +625,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
});
|
||||
signPSBTService.setOnFailed(workerStateEvent -> {
|
||||
setError("Signing Error", signPSBTService.getException().getMessage());
|
||||
log.error("Signing Error: " + signPSBTService.getException().getMessage());
|
||||
log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException());
|
||||
signButton.setDisable(false);
|
||||
});
|
||||
setDescription("Signing...");
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -69,10 +70,17 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
|
||||
tooltip.setText(getTooltip(transactionEntry));
|
||||
setTooltip(tooltip);
|
||||
|
||||
if(transactionEntry.getBlockTransaction().getHeight() <= 0) {
|
||||
tooltip.setOnShowing(event -> {
|
||||
tooltip.setText(getTooltip(transactionEntry));
|
||||
});
|
||||
}
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
Button viewTransactionButton = new Button("");
|
||||
viewTransactionButton.setGraphic(getViewTransactionGlyph());
|
||||
viewTransactionButton.setOnAction(event -> {
|
||||
@@ -106,11 +114,12 @@ 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");
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
Button receiveButton = new Button("");
|
||||
receiveButton.setGraphic(getReceiveGlyph());
|
||||
receiveButton.setOnAction(event -> {
|
||||
@@ -145,6 +154,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
setTooltip(tooltip);
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
Button viewTransactionButton = new Button("");
|
||||
viewTransactionButton.setGraphic(getViewTransactionGlyph());
|
||||
viewTransactionButton.setOnAction(event -> {
|
||||
@@ -326,6 +336,30 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
|
||||
}
|
||||
|
||||
private String getTooltip(TransactionEntry transactionEntry) {
|
||||
String tooltip = transactionEntry.getBlockTransaction().getHash().toString();
|
||||
if(transactionEntry.getBlockTransaction().getHeight() <= 0) {
|
||||
if(!AppServices.getMempoolHistogram().isEmpty()) {
|
||||
Set<MempoolRateSize> rateSizes = AppServices.getMempoolHistogram().get(AppServices.getMempoolHistogram().lastKey());
|
||||
double vSize = transactionEntry.getBlockTransaction().getTransaction().getVirtualSize();
|
||||
double feeRate = transactionEntry.getBlockTransaction().getFee() / vSize;
|
||||
long vSizefromTip = rateSizes.stream().filter(rateSize -> rateSize.getFee() > feeRate).mapToLong(MempoolRateSize::getVSize).sum();
|
||||
String amount = vSizefromTip + " vB";
|
||||
if(vSizefromTip > 1000 * 1000) {
|
||||
amount = String.format("%.2f", (double)vSizefromTip / (1000 * 1000)) + " MvB";
|
||||
} else if(vSizefromTip > 1000) {
|
||||
amount = String.format("%.2f", (double)vSizefromTip / 1000) + " kvB";
|
||||
}
|
||||
|
||||
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB (" + amount + " from tip)";
|
||||
}
|
||||
|
||||
tooltip += "\nRBF: " + (transactionEntry.getBlockTransaction().getTransaction().isReplaceByFee() ? "Enabled" : "Disabled");
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
private static Glyph getViewTransactionGlyph() {
|
||||
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
||||
searchGlyph.setFontSize(12);
|
||||
|
||||
@@ -42,12 +42,14 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
protected ButtonBase importButton;
|
||||
private final SimpleStringProperty password = new SimpleStringProperty("");
|
||||
private final boolean scannable;
|
||||
private final boolean fileFormatAvailable;
|
||||
protected List<Wallet> wallets;
|
||||
|
||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable) {
|
||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
|
||||
super(title, description, content, imageUrl);
|
||||
this.importer = importer;
|
||||
this.scannable = scannable;
|
||||
this.fileFormatAvailable = fileFormatAvailable;
|
||||
|
||||
buttonBox.getChildren().clear();
|
||||
buttonBox.getChildren().add(createButton());
|
||||
@@ -55,7 +57,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
if(scannable) {
|
||||
if(scannable && fileFormatAvailable) {
|
||||
ToggleButton scanButton = new ToggleButton("Scan...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
@@ -76,6 +78,16 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
SegmentedButton segmentedButton = new SegmentedButton();
|
||||
segmentedButton.getButtons().addAll(scanButton, fileButton);
|
||||
return segmentedButton;
|
||||
} else if(scannable) {
|
||||
importButton = new Button("Scan...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
importButton.setGraphic(cameraGlyph);
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setOnAction(event -> {
|
||||
importQR();
|
||||
});
|
||||
return importButton;
|
||||
} else {
|
||||
importButton = new Button("Import File...");
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -146,6 +158,15 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
try {
|
||||
importFile(importer.getName(), null, null);
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing QR", e);
|
||||
setError("Import Error", e.getMessage());
|
||||
}
|
||||
} else if(result.outputDescriptor != null) {
|
||||
wallets = List.of(result.outputDescriptor.toKeystoreWallet(null));
|
||||
try {
|
||||
importFile(importer.getName(), null, null);
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing QR", e);
|
||||
setError("Import Error", e.getMessage());
|
||||
}
|
||||
} else if(result.payload != null) {
|
||||
@@ -165,6 +186,9 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
} else if(result.exception != null) {
|
||||
log.error("Error importing QR", result.exception);
|
||||
setError("Import Error", result.exception.getMessage());
|
||||
} else {
|
||||
setError("Import Error", null);
|
||||
setExpanded(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,7 +214,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
protected abstract void importFile(String fileName, InputStream inputStream, String password) throws ImportException;
|
||||
|
||||
private Node getPasswordEntry(File file) {
|
||||
CustomPasswordField passwordField = (CustomPasswordField) TextFields.createClearablePasswordField();
|
||||
CustomPasswordField passwordField = new ViewPasswordField();
|
||||
passwordField.setPromptText("Wallet password");
|
||||
password.bind(passwordField.textProperty());
|
||||
HBox.setHgrow(passwordField, Priority.ALWAYS);
|
||||
|
||||
@@ -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) {
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane {
|
||||
private final WalletImport importer;
|
||||
|
||||
public FileWalletImportPane(WalletImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable());
|
||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), true);
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.util.StringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -39,7 +40,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private byte[] fileBytes;
|
||||
|
||||
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
@@ -97,13 +98,29 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
|
||||
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
|
||||
Label label = new Label("Script Type:");
|
||||
|
||||
HBox fieldBox = new HBox(5);
|
||||
fieldBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
|
||||
if(scriptTypes.contains(ScriptType.P2WPKH)) {
|
||||
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
|
||||
}
|
||||
scriptTypeComboBox.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(ScriptType scriptType) {
|
||||
return scriptType == null ? "" : scriptType.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
scriptTypeComboBox.setMaxWidth(170);
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
|
||||
|
||||
Region region = new Region();
|
||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||
@@ -130,7 +147,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().addAll(label, scriptTypeComboBox, helpLabel, region, importFileButton);
|
||||
contentBox.getChildren().addAll(label, fieldBox, region, importFileButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
|
||||
@@ -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.passphrase = (CustomPasswordField) TextFields.createClearablePasswordField();
|
||||
this(walletName, keystore, false);
|
||||
}
|
||||
|
||||
public KeystorePassphraseDialog(String walletName, Keystore keystore, boolean confirm) {
|
||||
this.passphrase = new ViewPasswordField();
|
||||
|
||||
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);
|
||||
|
||||
|
||||
@@ -92,18 +92,15 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
});
|
||||
getItems().add(copyLabel);
|
||||
|
||||
Object content = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
|
||||
if(content instanceof String) {
|
||||
MenuItem pasteLabel = new MenuItem("Paste Label");
|
||||
pasteLabel.setOnAction(AE -> {
|
||||
hide();
|
||||
Object currentContent = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
|
||||
if(currentContent instanceof String) {
|
||||
entry.labelProperty().set((String)currentContent);
|
||||
}
|
||||
});
|
||||
getItems().add(pasteLabel);
|
||||
}
|
||||
MenuItem pasteLabel = new MenuItem("Paste Label");
|
||||
pasteLabel.setOnAction(AE -> {
|
||||
hide();
|
||||
Object currentContent = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
|
||||
if(currentContent instanceof String) {
|
||||
entry.labelProperty().set((String)currentContent);
|
||||
}
|
||||
});
|
||||
getItems().add(pasteLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -79,9 +79,9 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
});
|
||||
|
||||
long previousFeeRate = 0;
|
||||
for(Long feeRate : SendController.FEE_RATES_RANGE) {
|
||||
for(Long feeRate : AppServices.FEE_RATES_RANGE) {
|
||||
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
||||
series.setName(feeRate + "+ vB");
|
||||
series.setName(feeRate + "+ sats/vB");
|
||||
long seriesTotalVSize = 0;
|
||||
|
||||
for(Date date : periodRateSizes.keySet()) {
|
||||
|
||||
@@ -103,6 +103,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
|
||||
|
||||
@@ -120,6 +121,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
fieldset.setSpacing(10);
|
||||
|
||||
Field addressField = new Field();
|
||||
addressField.setText("Address:");
|
||||
|
||||
@@ -10,11 +10,16 @@ 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.Timeline;
|
||||
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);
|
||||
@@ -40,20 +45,15 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
setContextMenu(null);
|
||||
}
|
||||
|
||||
if(mixStatus.getMixProgress() != null) {
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText("Pool: " + mixStatus.getMixProgress().getPoolId().replace("btc", " BTC"));
|
||||
setTooltip(tooltip);
|
||||
}
|
||||
|
||||
if(mixStatus.getNextMixUtxo() != null) {
|
||||
setMixSuccess(mixStatus.getNextMixUtxo());
|
||||
} else if(mixStatus.getMixFailReason() != null) {
|
||||
setMixFail(mixStatus.getMixFailReason());
|
||||
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
|
||||
} else if(mixStatus.getMixProgress() != null) {
|
||||
setMixProgress(mixStatus.getMixProgress());
|
||||
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,27 +67,72 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
setTooltip(tt);
|
||||
}
|
||||
|
||||
private void setMixFail(MixFailReason mixFailReason) {
|
||||
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
|
||||
if(mixFailReason != MixFailReason.CANCEL) {
|
||||
setGraphic(getFailGlyph());
|
||||
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());
|
||||
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);
|
||||
|
||||
Duration fadeDuration = Duration.millis(ERROR_DISPLAY_MILLIS - elapsed);
|
||||
double fadeFromValue = 1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS);
|
||||
Timeline timeline = AnimationUtil.getSlowFadeOut(failGlyph, fadeDuration, fadeFromValue, 10);
|
||||
timeline.setOnFinished(event -> {
|
||||
setTooltip(null);
|
||||
});
|
||||
timeline.play();
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void setMixProgress(MixProgress mixProgress) {
|
||||
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();
|
||||
tt.setText(mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.ImagePattern;
|
||||
import javafx.scene.shape.Circle;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class PayNymAvatar extends StackPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(PayNymAvatar.class);
|
||||
|
||||
private final ObjectProperty<PaymentCode> paymentCodeProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private static final Map<String, Image> paymentCodeCache = Collections.synchronizedMap(new HashMap<>());
|
||||
private static final Map<String, Object> paymentCodeLoading = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
public PayNymAvatar() {
|
||||
super();
|
||||
|
||||
paymentCodeProperty.addListener((observable, oldValue, paymentCode) -> {
|
||||
if(paymentCode == null) {
|
||||
getChildren().clear();
|
||||
} else if(Config.get().isUsePayNym() && (oldValue == null || !oldValue.toString().equals(paymentCode.toString()))) {
|
||||
String cacheId = getCacheId(paymentCode, getPrefWidth());
|
||||
if(paymentCodeCache.containsKey(cacheId)) {
|
||||
setImage(paymentCodeCache.get(cacheId));
|
||||
} else {
|
||||
PayNymAvatarService payNymAvatarService = new PayNymAvatarService(paymentCode, getPrefWidth());
|
||||
payNymAvatarService.setOnRunning(runningEvent -> {
|
||||
getChildren().clear();
|
||||
});
|
||||
payNymAvatarService.setOnSucceeded(successEvent -> {
|
||||
setImage(payNymAvatarService.getValue());
|
||||
});
|
||||
payNymAvatarService.setOnFailed(failedEvent -> {
|
||||
log.error("Error", failedEvent.getSource().getException());
|
||||
});
|
||||
payNymAvatarService.start();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setImage(Image image) {
|
||||
getChildren().clear();
|
||||
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
|
||||
circle.setFill(new ImagePattern(image));
|
||||
getChildren().add(circle);
|
||||
}
|
||||
|
||||
public PaymentCode getPaymentCode() {
|
||||
return paymentCodeProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<PaymentCode> paymentCodeProperty() {
|
||||
return paymentCodeProperty;
|
||||
}
|
||||
|
||||
public void setPaymentCode(PaymentCode paymentCode) {
|
||||
this.paymentCodeProperty.set(paymentCode);
|
||||
}
|
||||
|
||||
private static String getCacheId(PaymentCode paymentCode, double width) {
|
||||
return paymentCode.toString();
|
||||
}
|
||||
|
||||
private static class PayNymAvatarService extends Service<Image> {
|
||||
private final PaymentCode paymentCode;
|
||||
private final double width;
|
||||
|
||||
public PayNymAvatarService(PaymentCode paymentCode, double width) {
|
||||
this.paymentCode = paymentCode;
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Image> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Image call() throws Exception {
|
||||
String paymentCodeStr = paymentCode.toString();
|
||||
String cacheId = getCacheId(paymentCode, width);
|
||||
|
||||
Object lock = paymentCodeLoading.get(cacheId);
|
||||
if(lock != null) {
|
||||
synchronized(lock) {
|
||||
if(paymentCodeCache.containsKey(cacheId)) {
|
||||
return paymentCodeCache.get(cacheId);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lock = new Object();
|
||||
paymentCodeLoading.put(cacheId, lock);
|
||||
}
|
||||
|
||||
synchronized(lock) {
|
||||
String url = "https://paynym.is/" + paymentCodeStr + "/avatar";
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
|
||||
Image image = new Image(is, 150, 150, true, false);
|
||||
paymentCodeCache.put(cacheId, image);
|
||||
return image;
|
||||
} catch(Exception e) {
|
||||
log.debug("Error loading PayNym avatar", e);
|
||||
throw e;
|
||||
} finally {
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.soroban.PayNym;
|
||||
import com.sparrowwallet.sparrow.soroban.PayNymController;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
public class PayNymCell extends ListCell<PayNym> {
|
||||
private final PayNymController payNymController;
|
||||
|
||||
public PayNymCell(PayNymController payNymController) {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setContentDisplay(ContentDisplay.LEFT);
|
||||
getStyleClass().add("paynym-cell");
|
||||
setPrefHeight(50);
|
||||
this.payNymController = payNymController;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(PayNym payNym, boolean empty) {
|
||||
super.updateItem(payNym, empty);
|
||||
|
||||
if(empty || payNym == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(5, 5,5, 5));
|
||||
|
||||
PayNymAvatar payNymAvatar = new PayNymAvatar();
|
||||
payNymAvatar.setPrefWidth(30);
|
||||
payNymAvatar.setPrefHeight(30);
|
||||
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
||||
|
||||
HBox labelBox = new HBox();
|
||||
labelBox.setAlignment(Pos.CENTER);
|
||||
Label label = new Label(payNym.nymName(), payNymAvatar);
|
||||
label.setGraphicTextGap(10);
|
||||
labelBox.getChildren().add(label);
|
||||
pane.setLeft(labelBox);
|
||||
|
||||
if(getListView().getUserData() == Boolean.TRUE) {
|
||||
HBox hBox = new HBox();
|
||||
hBox.setAlignment(Pos.CENTER);
|
||||
Button button = new Button("Add Contact");
|
||||
hBox.getChildren().add(button);
|
||||
pane.setRight(hBox);
|
||||
button.setOnAction(event -> {
|
||||
button.setDisable(true);
|
||||
payNymController.followPayNym(payNym.paymentCode());
|
||||
});
|
||||
}
|
||||
|
||||
setText(null);
|
||||
setGraphic(pane);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
|
||||
public class PaymentCodeTextField extends CopyableTextField {
|
||||
private String paymentCodeStr;
|
||||
|
||||
public void setPaymentCode(PaymentCode paymentCode) {
|
||||
this.paymentCodeStr = paymentCode.toString();
|
||||
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
|
||||
setText(abbrevPcode);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getCopyText() {
|
||||
return paymentCodeStr;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.crypto.DumpedPrivateKey;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR;
|
||||
|
||||
public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
private static final Logger log = LoggerFactory.getLogger(PrivateKeySweepDialog.class);
|
||||
|
||||
private final TextArea key;
|
||||
private final ComboBox<ScriptType> keyScriptType;
|
||||
private final CopyableLabel keyAddress;
|
||||
private final ComboBoxTextField toAddress;
|
||||
private final ComboBox<Wallet> toWallet;
|
||||
|
||||
public PrivateKeySweepDialog(Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText("Sweep Private Key");
|
||||
|
||||
Image image = new Image("image/seed.png", 50, 50, false, false);
|
||||
if(!image.isError()) {
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setSmooth(false);
|
||||
imageView.setImage(image);
|
||||
dialogPane.setGraphic(imageView);
|
||||
}
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
fieldset.setSpacing(10);
|
||||
|
||||
Field keyField = new Field();
|
||||
keyField.setText("Private Key:");
|
||||
key = new TextArea();
|
||||
key.setWrapText(true);
|
||||
key.setPromptText("Wallet Import Format (WIF)");
|
||||
key.setPrefRowCount(2);
|
||||
key.getStyleClass().add("fixed-width");
|
||||
HBox keyBox = new HBox(5);
|
||||
VBox keyButtonBox = new VBox(5);
|
||||
Button scanKey = new Button("", getGlyph(FontAwesome5.Glyph.CAMERA));
|
||||
scanKey.setOnAction(event -> scanPrivateKey());
|
||||
Button readKey = new Button("", getGlyph(FontAwesome5.Glyph.FILE_IMPORT));
|
||||
readKey.setOnAction(event -> readPrivateKey());
|
||||
keyButtonBox.getChildren().addAll(scanKey, readKey);
|
||||
keyBox.getChildren().addAll(key, keyButtonBox);
|
||||
HBox.setHgrow(key, Priority.ALWAYS);
|
||||
keyField.getInputs().add(keyBox);
|
||||
|
||||
Field keyScriptTypeField = new Field();
|
||||
keyScriptTypeField.setText("Script Type:");
|
||||
keyScriptType = new ComboBox<>();
|
||||
keyScriptType.setItems(FXCollections.observableList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
|
||||
keyScriptTypeField.getInputs().add(keyScriptType);
|
||||
|
||||
keyScriptType.setConverter(new StringConverter<ScriptType>() {
|
||||
@Override
|
||||
public String toString(ScriptType scriptType) {
|
||||
return scriptType == null ? "" : scriptType.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
Field addressField = new Field();
|
||||
addressField.setText("Address:");
|
||||
keyAddress = new CopyableLabel();
|
||||
keyAddress.getStyleClass().add("fixed-width");
|
||||
addressField.getInputs().add(keyAddress);
|
||||
|
||||
Field toAddressField = new Field();
|
||||
toAddressField.setText("Sweep to:");
|
||||
toAddress = new ComboBoxTextField();
|
||||
toAddress.getStyleClass().add("fixed-width");
|
||||
toWallet = new ComboBox<>();
|
||||
toWallet.setItems(FXCollections.observableList(new ArrayList<>(AppServices.get().getOpenWallets().keySet())));
|
||||
toAddress.setComboProperty(toWallet);
|
||||
toWallet.prefWidthProperty().bind(toAddress.widthProperty());
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().addAll(toWallet, toAddress);
|
||||
toAddressField.getInputs().add(stackPane);
|
||||
|
||||
fieldset.getChildren().addAll(keyField, keyScriptTypeField, addressField, toAddressField);
|
||||
form.getChildren().add(fieldset);
|
||||
dialogPane.setContent(form);
|
||||
|
||||
ButtonType createButtonType = new javafx.scene.control.ButtonType("Create Transaction", ButtonBar.ButtonData.APPLY);
|
||||
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType, createButtonType);
|
||||
|
||||
Button createButton = (Button) dialogPane.lookupButton(createButtonType);
|
||||
createButton.setDefaultButton(true);
|
||||
createButton.setDisable(true);
|
||||
createButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
createTransaction();
|
||||
event.consume();
|
||||
});
|
||||
|
||||
key.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
boolean isValidKey = isValidKey();
|
||||
createButton.setDisable(!isValidKey || !isValidToAddress());
|
||||
if(isValidKey) {
|
||||
setFromAddress();
|
||||
}
|
||||
});
|
||||
|
||||
keyScriptType.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(isValidKey()) {
|
||||
setFromAddress();
|
||||
}
|
||||
});
|
||||
|
||||
toAddress.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
createButton.setDisable(!isValidKey() || !isValidToAddress());
|
||||
});
|
||||
|
||||
toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> {
|
||||
if(selectedWallet != null) {
|
||||
toAddress.setText(selectedWallet.getAddress(selectedWallet.getFreshNode(KeyPurpose.RECEIVE)).toString());
|
||||
}
|
||||
});
|
||||
|
||||
keyScriptType.setValue(ScriptType.P2PKH);
|
||||
if(wallet != null) {
|
||||
toAddress.setText(wallet.getAddress(wallet.getFreshNode(KeyPurpose.RECEIVE)).toString());
|
||||
}
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
setResultConverter(dialogButton -> null);
|
||||
dialogPane.setPrefWidth(680);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(key, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid private Key", !key.getText().isEmpty() && !isValidKey()));
|
||||
validationSupport.registerValidator(toAddress, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !toAddress.getText().isEmpty() && !isValidToAddress()));
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isValidKey() {
|
||||
try {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private DumpedPrivateKey getPrivateKey() {
|
||||
return DumpedPrivateKey.fromBase58(key.getText());
|
||||
}
|
||||
|
||||
private boolean isValidToAddress() {
|
||||
try {
|
||||
Address address = getToAddress();
|
||||
return true;
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Address getToAddress() throws InvalidAddressException {
|
||||
return Address.fromString(toAddress.getText());
|
||||
}
|
||||
|
||||
private void setFromAddress() {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
ScriptType scriptType = keyScriptType.getValue();
|
||||
Address address = scriptType.getAddress(privateKey.getKey());
|
||||
keyAddress.setText(address.toString());
|
||||
}
|
||||
|
||||
private void scanPrivateKey() {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
Optional<QRScanDialog.Result> result = qrScanDialog.showAndWait();
|
||||
if(result.isPresent() && result.get().payload != null) {
|
||||
key.setText(result.get().payload);
|
||||
}
|
||||
}
|
||||
|
||||
private void readPrivateKey() {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Private Key File");
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
if(file != null) {
|
||||
if(file.length() > 1024) {
|
||||
AppServices.showErrorDialog("Invalid private key file", "This file does not contain a valid private key.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
key.setText(Files.asCharSource(file, StandardCharsets.UTF_8).read().trim());
|
||||
} catch(IOException e) {
|
||||
AppServices.showErrorDialog("Error reading private key file", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createTransaction() {
|
||||
try {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
ScriptType scriptType = keyScriptType.getValue();
|
||||
Address fromAddress = scriptType.getAddress(privateKey.getKey());
|
||||
Address destAddress = getToAddress();
|
||||
|
||||
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress);
|
||||
addressUtxosService.setOnSucceeded(successEvent -> {
|
||||
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
|
||||
});
|
||||
addressUtxosService.setOnFailed(failedEvent -> {
|
||||
log.error("Error retrieving outputs for address " + fromAddress, failedEvent.getSource().getException());
|
||||
AppServices.showErrorDialog("Error retrieving outputs for address", failedEvent.getSource().getException().getMessage());
|
||||
});
|
||||
addressUtxosService.start();
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating sweep transaction", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Address destAddress) {
|
||||
ECKey pubKey = ECKey.fromPublicOnly(privKey);
|
||||
|
||||
Transaction noFeeTransaction = new Transaction();
|
||||
long total = 0;
|
||||
for(TransactionOutput txOutput : txOutputs) {
|
||||
scriptType.addSpendingInput(noFeeTransaction, txOutput, pubKey, TransactionSignature.dummy(scriptType == P2TR ? TransactionSignature.Type.SCHNORR : TransactionSignature.Type.ECDSA));
|
||||
total += txOutput.getValue();
|
||||
}
|
||||
|
||||
TransactionOutput sweepOutput = new TransactionOutput(noFeeTransaction, total, destAddress.getOutputScript());
|
||||
noFeeTransaction.addOutput(sweepOutput);
|
||||
|
||||
Double feeRate = AppServices.getDefaultFeeRate();
|
||||
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
||||
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
fee++;
|
||||
}
|
||||
|
||||
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||
if(total - fee <= dustThreshold) {
|
||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||
return;
|
||||
}
|
||||
|
||||
Transaction transaction = new Transaction();
|
||||
transaction.setVersion(2);
|
||||
transaction.setLocktime(AppServices.getCurrentBlockHeight() == null ? 0 : AppServices.getCurrentBlockHeight());
|
||||
for(TransactionInput txInput : noFeeTransaction.getInputs()) {
|
||||
transaction.addInput(txInput);
|
||||
}
|
||||
transaction.addOutput(new TransactionOutput(transaction, total - fee, destAddress.getOutputScript()));
|
||||
|
||||
PSBT psbt = new PSBT(transaction);
|
||||
for(int i = 0; i < txOutputs.size(); i++) {
|
||||
TransactionOutput utxoOutput = txOutputs.get(i);
|
||||
TransactionInput txInput = transaction.getInputs().get(i);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
|
||||
psbtInput.setWitnessUtxo(utxoOutput);
|
||||
|
||||
if(ScriptType.P2SH.isScriptType(utxoOutput.getScript())) {
|
||||
psbtInput.setRedeemScript(txInput.getScriptSig().getFirstNestedScript());
|
||||
}
|
||||
|
||||
if(txInput.getWitness() != null) {
|
||||
psbtInput.setWitnessScript(txInput.getWitness().getWitnessScript());
|
||||
}
|
||||
|
||||
if(!psbtInput.sign(scriptType.getOutputKey(privKey))) {
|
||||
AppServices.showErrorDialog("Failed to sign", "Failed to sign for transaction output " + utxoOutput.getHash() + ":" + utxoOutput.getIndex());
|
||||
return;
|
||||
}
|
||||
|
||||
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
|
||||
|
||||
Transaction finalizeTransaction = new Transaction();
|
||||
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
|
||||
psbtInput.setFinalScriptSig(finalizedTxInput.getScriptSig());
|
||||
psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness());
|
||||
}
|
||||
|
||||
setResult(psbt.extractTransaction());
|
||||
}
|
||||
|
||||
public Glyph getGlyph(FontAwesome5.Glyph glyphEnum) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphEnum);
|
||||
glyph.setFontSize(12);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.util.Duration;
|
||||
|
||||
public class ProgressTimer extends ProgressIndicator {
|
||||
private final IntegerProperty secondsProperty = new SimpleIntegerProperty(60);
|
||||
|
||||
private Timeline timeline;
|
||||
|
||||
public ProgressTimer() {
|
||||
super(0);
|
||||
getStyleClass().add("progress-timer");
|
||||
}
|
||||
|
||||
public void start() {
|
||||
start(e -> {});
|
||||
}
|
||||
|
||||
public void start(EventHandler<ActionEvent> onFinished) {
|
||||
timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(progressProperty(), 0)),
|
||||
new KeyFrame(Duration.seconds(getSeconds() * 0.8), e -> getStyleClass().add("warn")),
|
||||
new KeyFrame(Duration.seconds(getSeconds()), onFinished, new KeyValue(progressProperty(), 1)));
|
||||
timeline.setCycleCount(1);
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if(timeline != null) {
|
||||
timeline.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public int getSeconds() {
|
||||
return secondsProperty.get();
|
||||
}
|
||||
|
||||
public IntegerProperty secondsProperty() {
|
||||
return secondsProperty;
|
||||
}
|
||||
|
||||
public void setSeconds(int secondsProperty) {
|
||||
this.secondsProperty.set(secondsProperty);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||
import com.sparrowwallet.drongo.address.P2SHAddress;
|
||||
import com.sparrowwallet.drongo.address.P2WPKHAddress;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.Base43;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
@@ -479,18 +481,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
List<Wallet> wallets = new ArrayList<>();
|
||||
String masterFingerprint = Utils.bytesToHex(cryptoAccount.getMasterFingerprint());
|
||||
for(CryptoOutput cryptoOutput : cryptoAccount.getOutputDescriptors()) {
|
||||
Wallet wallet = new Wallet();
|
||||
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
|
||||
if(outputDescriptor.isMultisig()) {
|
||||
throw new IllegalStateException("Multisig output descriptors are unsupported in CryptoAccount");
|
||||
}
|
||||
|
||||
ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey();
|
||||
wallet.setScriptType(outputDescriptor.getScriptType());
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(outputDescriptor.getKeyDerivation(extendedKey).getDerivation())));
|
||||
keystore.setExtendedPublicKey(extendedKey);
|
||||
wallet.getKeystores().add(keystore);
|
||||
Wallet wallet = outputDescriptor.toKeystoreWallet(masterFingerprint);
|
||||
wallets.add(wallet);
|
||||
}
|
||||
|
||||
@@ -499,7 +491,9 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
private ScriptType getScriptType(List<ScriptExpression> scriptExpressions) {
|
||||
List<ScriptExpression> expressions = new ArrayList<>(scriptExpressions);
|
||||
if(expressions.get(expressions.size() - 1) == ScriptExpression.MULTISIG || expressions.get(expressions.size() - 1) == ScriptExpression.SORTED_MULTISIG) {
|
||||
if(expressions.get(expressions.size() - 1) == ScriptExpression.MULTISIG
|
||||
|| expressions.get(expressions.size() - 1) == ScriptExpression.SORTED_MULTISIG
|
||||
|| expressions.get(expressions.size() - 1) == ScriptExpression.COSIGNER) {
|
||||
expressions.remove(expressions.size() - 1);
|
||||
}
|
||||
|
||||
@@ -515,6 +509,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
return ScriptType.P2SH_P2WSH;
|
||||
} else if(List.of(ScriptExpression.WITNESS_SCRIPT_HASH).equals(expressions)) {
|
||||
return ScriptType.P2WSH;
|
||||
} else if(List.of(ScriptExpression.TAPROOT).equals(expressions)) {
|
||||
return ScriptType.P2TR;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Unknown script of " + expressions);
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SearchWalletDialog extends Dialog<Entry> {
|
||||
private static final Logger log = LoggerFactory.getLogger(SearchWalletDialog.class);
|
||||
|
||||
private final List<WalletForm> walletForms;
|
||||
private final TextField search;
|
||||
private final CoinTreeTable results;
|
||||
|
||||
public SearchWalletDialog(List<WalletForm> walletForms) {
|
||||
this.walletForms = walletForms;
|
||||
|
||||
if(walletForms.isEmpty()) {
|
||||
throw new IllegalArgumentException("No wallets selected to search");
|
||||
}
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("search.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText("Search Wallet");
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
if(!image.isError()) {
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setSmooth(false);
|
||||
imageView.setImage(image);
|
||||
dialogPane.setGraphic(imageView);
|
||||
}
|
||||
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(20);
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
fieldset.setSpacing(10);
|
||||
|
||||
Field searchField = new Field();
|
||||
searchField.setText("Search:");
|
||||
search = TextFields.createClearableTextField();
|
||||
search.setPromptText("Label, address, value or transaction ID");
|
||||
searchField.getInputs().add(search);
|
||||
|
||||
fieldset.getChildren().addAll(searchField);
|
||||
form.getChildren().add(fieldset);
|
||||
|
||||
results = new CoinTreeTable();
|
||||
results.setShowRoot(false);
|
||||
results.setPrefWidth(walletForms.size() > 1 ? 950 : 850);
|
||||
results.setBitcoinUnit(walletForms.iterator().next().getWallet());
|
||||
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
results.setPlaceholder(new Label("No results"));
|
||||
|
||||
if(walletForms.size() > 1) {
|
||||
TreeTableColumn<Entry, String> walletColumn = new TreeTableColumn<>("Wallet");
|
||||
walletColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getWallet().getDisplayName());
|
||||
});
|
||||
results.getColumns().add(walletColumn);
|
||||
}
|
||||
|
||||
TreeTableColumn<Entry, String> typeColumn = new TreeTableColumn<>("Type");
|
||||
typeColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getEntryType());
|
||||
});
|
||||
results.getColumns().add(typeColumn);
|
||||
|
||||
TreeTableColumn<Entry, Entry> entryCol = new TreeTableColumn<>("Date / Address / Output");
|
||||
entryCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
|
||||
});
|
||||
entryCol.setCellFactory(p -> new SearchEntryCell());
|
||||
String address = walletForms.iterator().next().getNodeEntry(KeyPurpose.RECEIVE).getAddress().toString();
|
||||
entryCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
|
||||
results.getColumns().add(entryCol);
|
||||
|
||||
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
|
||||
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
return param.getValue().getValue().labelProperty();
|
||||
});
|
||||
labelCol.setCellFactory(p -> new SearchLabelCell());
|
||||
results.getColumns().add(labelCol);
|
||||
|
||||
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
|
||||
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
|
||||
});
|
||||
amountCol.setCellFactory(p -> new CoinCell());
|
||||
results.getColumns().add(amountCol);
|
||||
|
||||
vBox.getChildren().addAll(form, results);
|
||||
dialogPane.setContent(vBox);
|
||||
|
||||
ButtonType showButtonType = new javafx.scene.control.ButtonType("Show", ButtonBar.ButtonData.APPLY);
|
||||
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType, showButtonType);
|
||||
|
||||
Button showButton = (Button) dialogPane.lookupButton(showButtonType);
|
||||
showButton.setDefaultButton(true);
|
||||
showButton.setDisable(true);
|
||||
|
||||
setResultConverter(buttonType -> buttonType == showButtonType ? results.getSelectionModel().getSelectedItem().getValue() : null);
|
||||
|
||||
results.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> {
|
||||
showButton.setDisable(results.getSelectionModel().getSelectedCells().isEmpty());
|
||||
});
|
||||
|
||||
search.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
searchWallet(newValue.toLowerCase());
|
||||
});
|
||||
|
||||
setResizable(true);
|
||||
|
||||
Platform.runLater(search::requestFocus);
|
||||
}
|
||||
|
||||
private void searchWallet(String searchText) {
|
||||
List<Entry> matchingEntries = new ArrayList<>();
|
||||
|
||||
if(!searchText.isEmpty()) {
|
||||
Long searchValue = null;
|
||||
|
||||
try {
|
||||
searchValue = Math.abs(Long.parseLong(searchText));
|
||||
} catch(NumberFormatException e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
for(WalletForm walletForm : walletForms) {
|
||||
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase().contains(searchText)) ||
|
||||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase().contains(searchText)) ||
|
||||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
if(hashIndexEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase().contains(searchText)) ||
|
||||
(hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.iterator().next().getWallet(), matchingEntries);
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
results.setRoot(rootItem);
|
||||
}
|
||||
|
||||
private static class SearchWalletEntry extends Entry {
|
||||
public SearchWalletEntry(Wallet wallet, List<Entry> entries) {
|
||||
super(wallet, wallet.getName(), entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getValue() {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEntryType() {
|
||||
return "Search Wallet Results";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function getWalletFunction() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SearchEntryCell extends EntryCell {
|
||||
@Override
|
||||
protected void updateItem(Entry entry, boolean empty) {
|
||||
super.updateItem(entry, empty);
|
||||
setContextMenu(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SearchLabelCell extends LabelCell {
|
||||
@Override
|
||||
public void updateItem(String label, boolean empty) {
|
||||
super.updateItem(label, empty);
|
||||
setContextMenu(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,4 +34,18 @@ public class TextUtils {
|
||||
helper.setText(DEFAULT_TEXT);
|
||||
return d;
|
||||
}
|
||||
|
||||
public static double computeTextHeight(Font font, String text) {
|
||||
helper.setText(text);
|
||||
helper.setFont(font);
|
||||
|
||||
helper.setWrappingWidth(0.0D);
|
||||
helper.setLineSpacing(0.0D);
|
||||
double d = Math.ceil(helper.getLayoutBounds().getHeight());
|
||||
|
||||
helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH);
|
||||
helper.setLineSpacing(DEFAULT_LINE_SPACING);
|
||||
helper.setText(DEFAULT_TEXT);
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -27,13 +27,16 @@ public class UsbStatusButton extends MenuButton {
|
||||
public void setDevices(List<Device> devices) {
|
||||
for(Device device : devices) {
|
||||
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
|
||||
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.KEEPKEY)) {
|
||||
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
|
||||
deviceItem = new Menu(device.getModel().toDisplayString());
|
||||
MenuItem toggleItem = new MenuItem("Toggle Passphrase " + (device.isNeedsPassphraseSent() ? "Off" : "On"));
|
||||
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
|
||||
toggleItem.setOnAction(event -> {
|
||||
Hwi.TogglePassphraseService togglePassphraseService = new Hwi.TogglePassphraseService(device);
|
||||
togglePassphraseService.setOnSucceeded(event1 -> {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
if(!device.getModel().externalPassphraseEntry()) {
|
||||
AppServices.showAlertDialog("Reconnect device", "Reconnect your " + device.getModel().toDisplayString() + " to reset the passphrase.", Alert.AlertType.INFORMATION);
|
||||
}
|
||||
});
|
||||
togglePassphraseService.setOnFailed(event1 -> {
|
||||
AppServices.showErrorDialog("Error toggling passphrase", event1.getSource().getException().getMessage());
|
||||
|
||||
@@ -9,14 +9,18 @@ import com.sparrowwallet.sparrow.wallet.WalletUtxosEntry;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.*;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class UtxosChart extends BarChart<String, Number> {
|
||||
private static final int MAX_BARS = 8;
|
||||
private static final int MAX_BARS = 10;
|
||||
private static final String OTHER_CATEGORY = "Other";
|
||||
private static final int TOOLTIP_SHOW_DELAY = 50;
|
||||
|
||||
private List<Entry> selectedEntries;
|
||||
private int totalUtxos;
|
||||
@@ -57,9 +61,11 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
XYChart.Data<String, Number> existingData = utxoSeries.getData().get(i);
|
||||
if(!newData.getXValue().equals(existingData.getXValue()) || !newData.getYValue().equals(existingData.getYValue()) || (newData.getExtraValue() instanceof Entry && !newData.getExtraValue().equals(existingData.getExtraValue()))) {
|
||||
utxoSeries.getData().set(i, newData);
|
||||
installTooltip(newData);
|
||||
}
|
||||
} else {
|
||||
utxoSeries.getData().add(newData);
|
||||
installTooltip(newData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,12 +80,21 @@ 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) + "..." + "\n" + ((UtxoEntry)entry).getDescription() : entry.getLabel() + "\n" + ((UtxoEntry)entry).getDescription();
|
||||
return entry.getLabel() + "\n" + ((UtxoEntry)entry).getDescription();
|
||||
}
|
||||
|
||||
return ((UtxoEntry)entry).getDescription();
|
||||
}
|
||||
|
||||
private void installTooltip(XYChart.Data<String, Number> item) {
|
||||
Tooltip.uninstall(item.getNode(), null);
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", item.getYValue());
|
||||
Tooltip tooltip = new Tooltip(item.getXValue() + "\n" + satsValue + " sats");
|
||||
tooltip.setShowDelay(Duration.millis(TOOLTIP_SHOW_DELAY));
|
||||
Tooltip.install(item.getNode(), tooltip);
|
||||
}
|
||||
|
||||
public void select(List<Entry> entries) {
|
||||
Set<Node> selectedBars = lookupAll(".chart-bar.selected");
|
||||
for(Node selectedBar : selectedBars) {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Skin;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
|
||||
public class ViewPasswordField extends CustomPasswordField {
|
||||
public ViewPasswordField() {
|
||||
super();
|
||||
getStyleClass().add("view-password-text-field");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new ViewPasswordFieldSkin(this) {
|
||||
@Override
|
||||
public ObjectProperty<Node> leftProperty() {
|
||||
return ViewPasswordField.this.leftProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectProperty<Node> rightProperty() {
|
||||
return ViewPasswordField.this.rightProperty();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import impl.org.controlsfx.skin.CustomTextFieldSkin;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
|
||||
public abstract class ViewPasswordFieldSkin extends CustomTextFieldSkin {
|
||||
private static final Duration FADE_DURATION = Duration.millis(350);
|
||||
public static final char BULLET = '\u25CF';
|
||||
|
||||
private boolean mask = true;
|
||||
|
||||
public ViewPasswordFieldSkin(CustomPasswordField textField) {
|
||||
super(textField);
|
||||
|
||||
Region viewPasswordButton = new Region();
|
||||
viewPasswordButton.getStyleClass().addAll("graphic");
|
||||
StackPane viewPasswordButtonPane = new StackPane(viewPasswordButton);
|
||||
viewPasswordButtonPane.getStyleClass().addAll("view-password-button");
|
||||
viewPasswordButtonPane.setOpacity(0.0);
|
||||
viewPasswordButtonPane.setCursor(Cursor.DEFAULT);
|
||||
|
||||
viewPasswordButtonPane.setOnMouseReleased(e -> {
|
||||
if(mask) {
|
||||
viewPasswordButtonPane.getStyleClass().remove("view-password-button");
|
||||
viewPasswordButtonPane.getStyleClass().addAll("hide-password-button");
|
||||
mask = false;
|
||||
} else {
|
||||
viewPasswordButtonPane.getStyleClass().remove("hide-password-button");
|
||||
viewPasswordButtonPane.getStyleClass().addAll("view-password-button");
|
||||
mask = true;
|
||||
}
|
||||
textField.setText(textField.getText());
|
||||
textField.end();
|
||||
});
|
||||
|
||||
textField.rightProperty().set(viewPasswordButtonPane);
|
||||
|
||||
final FadeTransition fader = new FadeTransition(FADE_DURATION, viewPasswordButtonPane);
|
||||
fader.setCycleCount(1);
|
||||
|
||||
textField.textProperty().addListener(new InvalidationListener() {
|
||||
@Override
|
||||
public void invalidated(Observable arg0) {
|
||||
String text = textField.getText();
|
||||
boolean isTextEmpty = text == null || text.isEmpty();
|
||||
boolean isButtonVisible = fader.getNode().getOpacity() > 0;
|
||||
|
||||
if (isTextEmpty && isButtonVisible) {
|
||||
setButtonVisible(false);
|
||||
} else if (!isTextEmpty && !isButtonVisible) {
|
||||
setButtonVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void setButtonVisible( boolean visible ) {
|
||||
fader.setFromValue(visible? 0.0: 1.0);
|
||||
fader.setToValue(visible? 1.0: 0.0);
|
||||
fader.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String maskText(String txt) {
|
||||
if(getSkinnable() instanceof PasswordField && mask) {
|
||||
int n = txt.length();
|
||||
StringBuilder passwordBuilder = new StringBuilder(n);
|
||||
for (int i = 0; i < n; i++) {
|
||||
passwordBuilder.append(BULLET);
|
||||
}
|
||||
return passwordBuilder.toString();
|
||||
} else {
|
||||
return txt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 SpecterDIY());
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), 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);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
@@ -31,8 +30,8 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
|
||||
public WalletPasswordDialog(String walletName, PasswordRequirement requirement, boolean suggestChangePassword) {
|
||||
this.requirement = requirement;
|
||||
this.password = (CustomPasswordField)TextFields.createClearablePasswordField();
|
||||
this.passwordConfirm = (CustomPasswordField)TextFields.createClearablePasswordField();
|
||||
this.password = new ViewPasswordField();
|
||||
this.passwordConfirm = new ViewPasswordField();
|
||||
this.backupExisting = new CheckBox("Backup existing wallet first");
|
||||
this.changePassword = new CheckBox("Change password");
|
||||
this.deleteBackups = new CheckBox("Delete any backups");
|
||||
@@ -131,10 +130,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;
|
||||
|
||||
@@ -15,9 +15,12 @@ import javafx.concurrent.Task;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class WebcamService extends ScheduledService<Image> {
|
||||
@@ -89,16 +92,20 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
opening.set(false);
|
||||
}
|
||||
|
||||
BufferedImage bimg = cam.getImage();
|
||||
if(bimg == null) {
|
||||
BufferedImage originalImage = cam.getImage();
|
||||
if(originalImage == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Image image = SwingFXUtils.toFXImage(bimg, null);
|
||||
CroppedDimension cropped = getCroppedDimension(originalImage);
|
||||
BufferedImage croppedImage = originalImage.getSubimage(cropped.x, cropped.y, cropped.length, cropped.length);
|
||||
BufferedImage framedImage = getFramedImage(originalImage, cropped);
|
||||
|
||||
Image image = SwingFXUtils.toFXImage(framedImage, null);
|
||||
updateValue(image);
|
||||
|
||||
if(System.currentTimeMillis() > (lastQrSampleTime + QR_SAMPLE_PERIOD_MILLIS)) {
|
||||
readQR(bimg);
|
||||
readQR(originalImage, croppedImage);
|
||||
lastQrSampleTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@@ -125,18 +132,49 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
return super.cancel();
|
||||
}
|
||||
|
||||
private void readQR(BufferedImage bufferedImage) {
|
||||
private void readQR(BufferedImage wideImage, BufferedImage croppedImage) {
|
||||
Result result = readQR(wideImage);
|
||||
if(result == null) {
|
||||
result = readQR(croppedImage);
|
||||
}
|
||||
|
||||
if(result != null) {
|
||||
resultProperty.set(result);
|
||||
}
|
||||
}
|
||||
|
||||
private Result readQR(BufferedImage bufferedImage) {
|
||||
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
|
||||
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
|
||||
|
||||
try {
|
||||
Result result = qrReader.decode(bitmap);
|
||||
resultProperty.set(result);
|
||||
return qrReader.decode(bitmap, Map.of(DecodeHintType.TRY_HARDER, Boolean.TRUE));
|
||||
} catch(ReaderException e) {
|
||||
// fall thru, it means there is no QR code in image
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private BufferedImage getFramedImage(BufferedImage image, CroppedDimension cropped) {
|
||||
BufferedImage clone = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = (Graphics2D)clone.getGraphics();
|
||||
g2d.drawImage(image, 0, 0, null);
|
||||
float[] dash1 = {10.0f};
|
||||
g2d.setColor(Color.BLACK);
|
||||
g2d.setStroke(new BasicStroke(resolution == WebcamResolution.HD ? 3.0f : 1.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash1, 0.0f));
|
||||
g2d.draw(new RoundRectangle2D.Double(cropped.x, cropped.y, cropped.length, cropped.length, 10, 10));
|
||||
g2d.dispose();
|
||||
return clone;
|
||||
}
|
||||
|
||||
private CroppedDimension getCroppedDimension(BufferedImage bufferedImage) {
|
||||
int dimension = Math.min(bufferedImage.getWidth(), bufferedImage.getHeight());
|
||||
int squareSize = dimension / 2;
|
||||
int x = (bufferedImage.getWidth() - squareSize) / 2;
|
||||
int y = (bufferedImage.getHeight() - squareSize) / 2;
|
||||
return new CroppedDimension(x, y, squareSize);
|
||||
}
|
||||
|
||||
public Result getResult() {
|
||||
return resultProperty.get();
|
||||
}
|
||||
@@ -172,4 +210,16 @@ public class WebcamService extends ScheduledService<Image> {
|
||||
public BooleanProperty openingProperty() {
|
||||
return opening;
|
||||
}
|
||||
|
||||
private static class CroppedDimension {
|
||||
public int x;
|
||||
public int y;
|
||||
public int length;
|
||||
|
||||
public CroppedDimension(int x, int y, int length) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.length = length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.wallet.Function;
|
||||
|
||||
public class FunctionActionEvent {
|
||||
private final Function function;
|
||||
private final Wallet wallet;
|
||||
|
||||
public FunctionActionEvent(Function function, Wallet wallet) {
|
||||
this.function = function;
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Function getFunction() {
|
||||
return function;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public boolean selectFunction() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@ 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.HashIndexEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionHashIndexEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -60,12 +62,21 @@ public class NewWalletTransactionsEvent {
|
||||
return String.format(Locale.ENGLISH, "%,d", value) + " sats";
|
||||
}
|
||||
|
||||
public List<BlockTransaction> getWhirlpoolMixTransactions() {
|
||||
public List<BlockTransaction> getUnspentConfirmingWhirlpoolMixTransactions() {
|
||||
List<BlockTransaction> mixTransactions = new ArrayList<>();
|
||||
if(wallet.isWhirlpoolMixWallet()) {
|
||||
return transactionEntries.stream().filter(txEntry -> txEntry.getValue() == 0).map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
|
||||
return transactionEntries.stream()
|
||||
.filter(txEntry -> txEntry.getValue() == 0 && txEntry.isConfirming() && !allOutputsSpent(txEntry))
|
||||
.map(TransactionEntry::getBlockTransaction).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return mixTransactions;
|
||||
}
|
||||
|
||||
private boolean allOutputsSpent(TransactionEntry txEntry) {
|
||||
return txEntry.getChildren().stream()
|
||||
.map(refEntry -> ((TransactionHashIndexEntry)refEntry))
|
||||
.filter(txRefEntry -> txRefEntry.getType() == HashIndexEntry.Type.OUTPUT)
|
||||
.allMatch(txRefEntry -> txRefEntry.getHashIndex().isSpent());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.wallet.Function;
|
||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||
|
||||
public class ReceiveActionEvent {
|
||||
private final Wallet wallet;
|
||||
|
||||
public class ReceiveActionEvent extends FunctionActionEvent {
|
||||
public ReceiveActionEvent(NodeEntry receiveEntry) {
|
||||
this.wallet = receiveEntry.getWallet();
|
||||
super(Function.RECEIVE, receiveEntry.getWallet());
|
||||
}
|
||||
|
||||
public ReceiveActionEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
super(Function.RECEIVE, wallet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.net.Aopp;
|
||||
|
||||
public class ReceiveProofEvent {
|
||||
private final Wallet wallet;
|
||||
private final Aopp aopp;
|
||||
|
||||
public ReceiveProofEvent(Wallet wallet, Aopp aopp) {
|
||||
this.wallet = wallet;
|
||||
this.aopp = aopp;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public Aopp getAopp() {
|
||||
return aopp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
|
||||
public class SelectEntryEvent {
|
||||
private final Entry entry;
|
||||
|
||||
public SelectEntryEvent(Entry entry) {
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
public Entry getEntry() {
|
||||
return entry;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return entry.getWallet();
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,24 @@ package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.wallet.Function;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SendActionEvent {
|
||||
private final Wallet wallet;
|
||||
public class SendActionEvent extends FunctionActionEvent {
|
||||
private final List<BlockTransactionHashIndex> utxos;
|
||||
|
||||
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
this.wallet = wallet;
|
||||
super(Function.SEND, wallet);
|
||||
this.utxos = utxos;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public List<BlockTransactionHashIndex> getUtxos() {
|
||||
return utxos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean selectFunction() {
|
||||
return !getUtxos().isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@ public class SettingsChangedEvent {
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE;
|
||||
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class SorobanInitiatedEvent {
|
||||
private Wallet wallet;
|
||||
|
||||
public SorobanInitiatedEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ 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;
|
||||
@@ -23,15 +24,17 @@ 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, Long fee, Pool pool) {
|
||||
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;
|
||||
@@ -49,6 +52,10 @@ public class SpendUtxoEvent {
|
||||
return payments;
|
||||
}
|
||||
|
||||
public List<byte[]> getOpReturns() {
|
||||
return opReturns;
|
||||
}
|
||||
|
||||
public Long getFee() {
|
||||
return fee;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletDeletedEvent extends WalletChangedEvent {
|
||||
public WalletDeletedEvent(Wallet wallet) {
|
||||
super(wallet);
|
||||
}
|
||||
}
|
||||
@@ -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,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,16 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletWatchLastChangedEvent extends WalletSettingsChangedEvent {
|
||||
private final Integer watchLast;
|
||||
|
||||
public WalletWatchLastChangedEvent(Wallet wallet, Wallet pastWallet, String walletId, Integer watchLast) {
|
||||
super(wallet, pastWallet, walletId);
|
||||
this.watchLast = watchLast;
|
||||
}
|
||||
|
||||
public Integer getWatchLast() {
|
||||
return watchLast;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ public class WhirlpoolMixEvent {
|
||||
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;
|
||||
@@ -19,6 +20,7 @@ public class WhirlpoolMixEvent {
|
||||
this.mixProgress = mixProgress;
|
||||
this.nextUtxo = null;
|
||||
this.mixFailReason = null;
|
||||
this.mixError = null;
|
||||
}
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo) {
|
||||
@@ -27,14 +29,16 @@ public class WhirlpoolMixEvent {
|
||||
this.mixProgress = null;
|
||||
this.nextUtxo = nextUtxo;
|
||||
this.mixFailReason = null;
|
||||
this.mixError = null;
|
||||
}
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixFailReason mixFailReason) {
|
||||
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() {
|
||||
@@ -56,4 +60,8 @@ public class WhirlpoolMixEvent {
|
||||
public MixFailReason getMixFailReason() {
|
||||
return mixFailReason;
|
||||
}
|
||||
|
||||
public String getMixError() {
|
||||
return mixError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||
EYE('\uf06e'),
|
||||
FEATHER_ALT('\uf56b'),
|
||||
FILE_CSV('\uf6dd'),
|
||||
FILE_IMPORT('\uf56f'),
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||
HAND_HOLDING_WATER('\uf4c1'),
|
||||
@@ -56,12 +57,14 @@ public class FontAwesome5 extends GlyphFont {
|
||||
QUESTION_CIRCLE('\uf059'),
|
||||
RANDOM('\uf074'),
|
||||
REPLY_ALL('\uf122'),
|
||||
ROBOT('\uf544'),
|
||||
SATELLITE_DISH('\uf7c0'),
|
||||
SD_CARD('\uf7c2'),
|
||||
SEARCH('\uf002'),
|
||||
SIGN_OUT_ALT('\uf2f5'),
|
||||
SQUARE('\uf0c8'),
|
||||
SNOWFLAKE('\uf2dc'),
|
||||
SORT_NUMERIC_DOWN('\uf162'),
|
||||
SUN('\uf185'),
|
||||
THEATER_MASKS('\uf630'),
|
||||
TIMES_CIRCLE('\uf057'),
|
||||
@@ -69,7 +72,10 @@ public class FontAwesome5 extends GlyphFont {
|
||||
TOGGLE_ON('\uf205'),
|
||||
TOOLS('\uf7d9'),
|
||||
UNDO('\uf0e2'),
|
||||
USER('\uf007'),
|
||||
USER_FRIENDS('\uf500'),
|
||||
USER_PLUS('\uf234'),
|
||||
USER_SLASH('\uf506'),
|
||||
WALLET('\uf555'),
|
||||
WEIGHT('\uf496');
|
||||
|
||||
|
||||
@@ -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,7 +50,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(cf.name);
|
||||
wallet.setPolicyType(PolicyType.MULTI);
|
||||
ScriptType scriptType = ScriptType.valueOf(cf.addressType);
|
||||
ScriptType scriptType = ScriptType.valueOf(cf.addressType.replace('-', '_'));
|
||||
|
||||
for(ExtPublicKey extKey : cf.extendedPublicKeys) {
|
||||
Keystore keystore = new Keystore(extKey.name);
|
||||
@@ -78,7 +78,6 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
|
||||
return wallet;
|
||||
} catch(Exception e) {
|
||||
log.error("Error importing " + getName() + " wallet", e);
|
||||
throw new ImportException("Error importing " + getName() + " wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
|
||||
return keystore;
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting " + getName() + " keystore", e);
|
||||
throw new ImportException("Error getting " + getName() + " keystore", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -81,7 +85,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import file created by using the Settings > Multisig Wallets > Export XPUB feature on your Coldcard.";
|
||||
return "Import file created by using the Settings > Multisig Wallets > Export XPUB > 0 feature on your Coldcard.";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -150,7 +154,6 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
return wallet;
|
||||
} catch(Exception e) {
|
||||
log.error("Error importing " + getName() + " wallet", e);
|
||||
throw new ImportException("Error importing " + getName() + " wallet", e);
|
||||
}
|
||||
}
|
||||
@@ -193,7 +196,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
for(Keystore keystore : wallet.getKeystores()) {
|
||||
if(multipleDerivations) {
|
||||
writer.append("# derivation: ").append(keystore.getKeyDerivation().getDerivationPath()).append("\n");
|
||||
writer.append("Derivation: ").append(keystore.getKeyDerivation().getDerivationPath()).append("\n");
|
||||
}
|
||||
writer.append(keystore.getKeyDerivation().getMasterFingerprint().toUpperCase()).append(": ").append(keystore.getExtendedPublicKey().toString()).append("\n");
|
||||
if(multipleDerivations) {
|
||||
|
||||
@@ -29,7 +29,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON feature on your Coldcard. Note this requires firmware version 3.1.3 or later.";
|
||||
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON > 0 feature on your Coldcard. Note this requires firmware version 3.1.3 or later.";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -86,7 +86,6 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting " + getName() + " keystore", e);
|
||||
throw new ImportException("Error getting " + getName() + " keystore", e);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Arrays;
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.net.PagedBatchRequestBuilder.DEFAULT_PAGE_SIZE;
|
||||
import static com.sparrowwallet.sparrow.net.TcpTransport.DEFAULT_MAX_TIMEOUT;
|
||||
|
||||
public class Config {
|
||||
private static final Logger log = LoggerFactory.getLogger(Config.class);
|
||||
|
||||
@@ -40,8 +40,8 @@ public class Config {
|
||||
private boolean openWalletsInNewWindows = false;
|
||||
private boolean hideEmptyUsedAddresses = false;
|
||||
private boolean showTransactionHex = true;
|
||||
private boolean showLoadingLog = false;
|
||||
private boolean showUtxosChart = true;
|
||||
private boolean showLoadingLog = true;
|
||||
private boolean preventSleep = false;
|
||||
private List<File> recentWalletFiles;
|
||||
private Integer keyDerivationPeriod;
|
||||
private File hwi;
|
||||
@@ -50,15 +50,19 @@ public class Config {
|
||||
private ServerType serverType;
|
||||
private String publicElectrumServer;
|
||||
private String coreServer;
|
||||
private List<String> recentCoreServers;
|
||||
private CoreAuthType coreAuthType;
|
||||
private File coreDataDir;
|
||||
private String coreAuth;
|
||||
private Boolean coreMultiWallet;
|
||||
private String coreWallet;
|
||||
private String electrumServer;
|
||||
private List<String> recentElectrumServers;
|
||||
private File electrumServerCert;
|
||||
private boolean useProxy;
|
||||
private String proxyServer;
|
||||
private int maxServerTimeout = DEFAULT_MAX_TIMEOUT;
|
||||
private int maxPageSize = DEFAULT_PAGE_SIZE;
|
||||
private boolean usePayNym;
|
||||
private boolean sameAppMixing;
|
||||
private Double appWidth;
|
||||
private Double appHeight;
|
||||
|
||||
@@ -270,13 +274,12 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isShowUtxosChart() {
|
||||
return showUtxosChart;
|
||||
public boolean isPreventSleep() {
|
||||
return preventSleep;
|
||||
}
|
||||
|
||||
public void setShowUtxosChart(boolean showUtxosChart) {
|
||||
this.showUtxosChart = showUtxosChart;
|
||||
flush();
|
||||
public void setPreventSleep(boolean preventSleep) {
|
||||
this.preventSleep = preventSleep;
|
||||
}
|
||||
|
||||
public List<File> getRecentWalletFiles() {
|
||||
@@ -376,8 +379,10 @@ public class Config {
|
||||
}
|
||||
|
||||
public void changePublicServer() {
|
||||
List<String> otherServers = Arrays.stream(PublicElectrumServer.values()).map(PublicElectrumServer::getUrl).filter(url -> !url.equals(getPublicElectrumServer())).collect(Collectors.toList());
|
||||
setPublicElectrumServer(otherServers.get(new Random().nextInt(otherServers.size())));
|
||||
List<String> otherServers = PublicElectrumServer.getServers().stream().map(PublicElectrumServer::getUrl).filter(url -> !url.equals(getPublicElectrumServer())).collect(Collectors.toList());
|
||||
if(!otherServers.isEmpty()) {
|
||||
setPublicElectrumServer(otherServers.get(new Random().nextInt(otherServers.size())));
|
||||
}
|
||||
}
|
||||
|
||||
public String getCoreServer() {
|
||||
@@ -389,6 +394,23 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public List<String> getRecentCoreServers() {
|
||||
return recentCoreServers;
|
||||
}
|
||||
|
||||
public void addRecentCoreServer(String coreServer) {
|
||||
if(recentCoreServers == null) {
|
||||
recentCoreServers = new ArrayList<>();
|
||||
}
|
||||
|
||||
if(!recentCoreServers.contains(coreServer)) {
|
||||
recentCoreServers.stream().filter(url -> Objects.equals(Protocol.getHost(url), Protocol.getHost(coreServer)))
|
||||
.findFirst().ifPresent(existingUrl -> recentCoreServers.remove(existingUrl));
|
||||
recentCoreServers.add(coreServer);
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
public CoreAuthType getCoreAuthType() {
|
||||
return coreAuthType;
|
||||
}
|
||||
@@ -416,24 +438,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;
|
||||
}
|
||||
@@ -443,6 +447,31 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public List<String> getRecentElectrumServers() {
|
||||
return recentElectrumServers;
|
||||
}
|
||||
|
||||
public void addRecentServer() {
|
||||
if(serverType == ServerType.BITCOIN_CORE && coreServer != null) {
|
||||
addRecentCoreServer(coreServer);
|
||||
} else if(serverType == ServerType.ELECTRUM_SERVER && electrumServer != null) {
|
||||
addRecentElectrumServer(electrumServer);
|
||||
}
|
||||
}
|
||||
|
||||
public void addRecentElectrumServer(String electrumServer) {
|
||||
if(recentElectrumServers == null) {
|
||||
recentElectrumServers = new ArrayList<>();
|
||||
}
|
||||
|
||||
if(!recentElectrumServers.contains(electrumServer)) {
|
||||
recentElectrumServers.stream().filter(url -> Objects.equals(Protocol.getHost(url), Protocol.getHost(electrumServer)))
|
||||
.findFirst().ifPresent(existingUrl -> recentElectrumServers.remove(existingUrl));
|
||||
recentElectrumServers.add(electrumServer);
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
public File getElectrumServerCert() {
|
||||
return electrumServerCert;
|
||||
}
|
||||
@@ -470,6 +499,32 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public int getMaxServerTimeout() {
|
||||
return maxServerTimeout;
|
||||
}
|
||||
|
||||
public int getMaxPageSize() {
|
||||
return maxPageSize;
|
||||
}
|
||||
|
||||
public boolean isUsePayNym() {
|
||||
return usePayNym;
|
||||
}
|
||||
|
||||
public void setUsePayNym(boolean usePayNym) {
|
||||
this.usePayNym = usePayNym;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isSameAppMixing() {
|
||||
return sameAppMixing;
|
||||
}
|
||||
|
||||
public void setSameAppMixing(boolean sameAppMixing) {
|
||||
this.sameAppMixing = sameAppMixing;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Double getAppWidth() {
|
||||
return appWidth;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
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, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -264,7 +264,6 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
|
||||
return wallet;
|
||||
} catch (Exception e) {
|
||||
log.error("Error importing Electrum Wallet", e);
|
||||
throw new ImportException("Error importing Electrum Wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class GordianSeedTool implements KeystoreFileImport {
|
||||
@Override
|
||||
public boolean isEncrypted(File file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Gordian Seed Tool";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.GORDIAN_SEED_TOOL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
throw new ImportException("Only QR imports are supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Select your seed and scan the QR code created by Authenticate > Derive Key > Other Key Derivations > " + Network.get().toDisplayString() + " > Master Key > Account Descriptor. Click the share icon at the bottom.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFileFormatAvailable() {
|
||||
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;
|
||||
@@ -38,16 +39,17 @@ public class Hwi {
|
||||
private static boolean isPromptActive = false;
|
||||
|
||||
public List<Device> enumerate(String passphrase) throws ImportException {
|
||||
String output = null;
|
||||
try {
|
||||
List<String> command;
|
||||
if(passphrase != null && !passphrase.isEmpty()) {
|
||||
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", passphrase, Command.ENUMERATE.toString());
|
||||
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", escape(passphrase), Command.ENUMERATE.toString());
|
||||
} else {
|
||||
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString());
|
||||
}
|
||||
|
||||
isPromptActive = true;
|
||||
String output = execute(command);
|
||||
output = execute(command);
|
||||
Device[] devices = getGson().fromJson(output, Device[].class);
|
||||
if(devices == null) {
|
||||
throw new ImportException("Error scanning, check devices are ready");
|
||||
@@ -55,7 +57,19 @@ public class Hwi {
|
||||
return Arrays.stream(devices).filter(device -> device != null && device.getModel() != null).collect(Collectors.toList());
|
||||
} catch(IOException e) {
|
||||
log.error("Error executing " + HWI_VERSION_DIR, e);
|
||||
throw new ImportException(e);
|
||||
throw new ImportException("Error executing HWI", e);
|
||||
} catch(Exception e) {
|
||||
if(output != null) {
|
||||
try {
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
JsonElement error = result.get("error");
|
||||
throw new ImportException(error.getAsString());
|
||||
} catch(Exception ex) {
|
||||
log.error("Error parsing JSON: " + output, e);
|
||||
throw new ImportException("Error parsing JSON: " + output, e);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
@@ -91,6 +105,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;
|
||||
@@ -156,9 +179,9 @@ public class Hwi {
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceCommand(device, passphrase, Command.SIGN_MESSAGE, message, derivationPath));
|
||||
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
|
||||
} else {
|
||||
output = execute(getDeviceCommand(device, Command.SIGN_MESSAGE, message, derivationPath));
|
||||
output = execute(getDeviceArguments(device, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
|
||||
}
|
||||
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
@@ -186,9 +209,9 @@ public class Hwi {
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceCommand(device, passphrase, Command.SIGN_TX, psbtBase64));
|
||||
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
|
||||
} else {
|
||||
output = execute(getDeviceCommand(device, Command.SIGN_TX, psbtBase64));
|
||||
output = execute(getDeviceArguments(device, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
|
||||
}
|
||||
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
@@ -210,7 +233,7 @@ public class Hwi {
|
||||
} catch(IOException e) {
|
||||
throw new SignTransactionException("Could not sign PSBT", e);
|
||||
} catch(PSBTParseException e) {
|
||||
throw new SignTransactionException("Could not parsed signed PSBT", e);
|
||||
throw new SignTransactionException("Could not parse signed PSBT", e);
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
@@ -219,7 +242,31 @@ public class Hwi {
|
||||
private String execute(List<String> command) throws IOException {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||
Process process = processBuilder.start();
|
||||
return CharStreams.toString(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
|
||||
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
return CharStreams.toString(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private String execute(List<String> arguments, Command command, String... commandArguments) throws IOException {
|
||||
List<String> processArguments = new ArrayList<>(arguments);
|
||||
processArguments.add("--stdin");
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(processArguments);
|
||||
Process process = processBuilder.start();
|
||||
|
||||
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
|
||||
writer.write(command.toString());
|
||||
for(String commandArgument : commandArguments) {
|
||||
writer.write(" \"");
|
||||
writer.write(commandArgument.replace("\\", "\\\\").replace("\"", "\\\""));
|
||||
writer.write("\"");
|
||||
}
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
return CharStreams.toString(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized File getHwiExecutable(Command command) {
|
||||
@@ -377,30 +424,42 @@ public class Hwi {
|
||||
return result.get("success").getAsBoolean();
|
||||
}
|
||||
|
||||
private List<String> getDeviceArguments(Device device, Command command) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType()));
|
||||
addChainType(elements, false);
|
||||
return elements;
|
||||
}
|
||||
|
||||
private List<String> getDeviceArguments(Device device, String passphrase, Command command) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase)));
|
||||
addChainType(elements, false);
|
||||
return elements;
|
||||
}
|
||||
|
||||
private List<String> getDeviceCommand(Device device, Command command) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
|
||||
addChainType(elements);
|
||||
addChainType(elements, true);
|
||||
return elements;
|
||||
}
|
||||
|
||||
private List<String> getDeviceCommand(Device device, Command command, String... commandData) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
|
||||
addChainType(elements);
|
||||
addChainType(elements, true);
|
||||
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
return elements;
|
||||
}
|
||||
|
||||
private List<String> getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString()));
|
||||
addChainType(elements);
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase), command.toString()));
|
||||
addChainType(elements, true);
|
||||
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
return elements;
|
||||
}
|
||||
|
||||
private void addChainType(List<String> elements) {
|
||||
private void addChainType(List<String> elements, boolean commandPresent) {
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
elements.add(elements.size() - 1, "--chain");
|
||||
elements.add(elements.size() - 1, getChainName(Network.get()));
|
||||
elements.add(elements.size() - (commandPresent ? 1 : 0), "--chain");
|
||||
elements.add(elements.size() - (commandPresent ? 1 : 0), getChainName(Network.get()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,6 +473,15 @@ public class Hwi {
|
||||
return network.toString();
|
||||
}
|
||||
|
||||
private String escape(String passphrase) {
|
||||
Platform platform = Platform.getCurrent();
|
||||
if(platform == Platform.WINDOWS) {
|
||||
return passphrase.replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
public static class EnumerateService extends Service<List<Device>> {
|
||||
private final String passphrase;
|
||||
|
||||
@@ -580,6 +648,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;
|
||||
|
||||
@@ -62,10 +62,8 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
|
||||
|
||||
return keystore;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.error("Error getting " + getName() + " keystore - not an output descriptor");
|
||||
throw new ImportException("Error getting " + getName() + " keystore", e);
|
||||
throw new ImportException("Error getting " + getName() + " keystore - not an output descriptor", e);
|
||||
} catch (Exception e) {
|
||||
log.error("Error getting " + getName() + " keystore", e);
|
||||
throw new ImportException("Error getting " + getName() + " keystore", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,7 @@ import java.io.InputStream;
|
||||
public interface KeystoreFileImport extends KeystoreImport, FileImport {
|
||||
Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
|
||||
boolean isKeystoreImportScannable();
|
||||
default boolean isFileFormatAvailable() {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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 multisig wallets with a P2WSH script type.";
|
||||
return "Import QR created on your SeedSigner by selecting xPub from Seed in the Seed Tools menu once you have entered your seed.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -101,9 +101,12 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
java.nio.file.Files.copy(inputStream, tempFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
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;
|
||||
if(persistenceType != PersistenceType.JSON || !isEncrypted(tempFile)) {
|
||||
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();
|
||||
@@ -118,7 +121,6 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
|
||||
return wallet;
|
||||
} catch(IOException | StorageException e) {
|
||||
log.error("Error importing Sparrow wallet", e);
|
||||
throw new ImportException("Error importing Sparrow wallet", e);
|
||||
} finally {
|
||||
if(storage != null) {
|
||||
|
||||
@@ -35,7 +35,6 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
|
||||
|
||||
return keystore;
|
||||
} catch(IOException e) {
|
||||
log.error("Error getting " + getName() + " keystore", e);
|
||||
throw new ImportException("Error getting " + getName() + " keystore", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -93,7 +93,6 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
return wallet;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error importing " + getName() + " wallet", e);
|
||||
throw new ImportException("Error importing " + getName() + " wallet", e);
|
||||
}
|
||||
|
||||
@@ -161,5 +160,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -62,6 +64,10 @@ public class Storage {
|
||||
}
|
||||
|
||||
public boolean isEncrypted() throws IOException {
|
||||
if(!walletFile.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return persistence.isEncrypted(walletFile);
|
||||
}
|
||||
|
||||
@@ -168,10 +174,27 @@ public class Storage {
|
||||
deleteBackups(null);
|
||||
}
|
||||
|
||||
public void deleteTempBackups() {
|
||||
public void deleteTempBackups(boolean forceSave) {
|
||||
File[] backups = getBackups(Storage.TEMP_BACKUP_PREFIX);
|
||||
if(backups.length > 0 && (forceSave || hasStartedSince(backups[0]))) {
|
||||
File permanent = new File(backups[0].getParent(), backups[0].getName().substring(Storage.TEMP_BACKUP_PREFIX.length() + 1));
|
||||
backups[0].renameTo(permanent);
|
||||
}
|
||||
|
||||
deleteBackups(Storage.TEMP_BACKUP_PREFIX);
|
||||
}
|
||||
|
||||
private boolean hasStartedSince(File lastBackup) {
|
||||
try {
|
||||
Date date = BACKUP_DATE_FORMAT.parse(getBackupDate(lastBackup.getName()));
|
||||
ProcessHandle.Info processInfo = ProcessHandle.current().info();
|
||||
return (processInfo.startInstant().isPresent() && processInfo.startInstant().get().isAfter(date.toInstant()));
|
||||
} catch(Exception e) {
|
||||
log.error("Error parsing date for backup file " + lastBackup.getName(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteBackups(String prefix) {
|
||||
File[] backups = getBackups(prefix);
|
||||
for(File backup : backups) {
|
||||
@@ -355,7 +378,7 @@ public class Storage {
|
||||
}
|
||||
|
||||
public static PersistenceType detectPersistenceType(File walletFile) {
|
||||
try(Reader reader = new FileReader(walletFile)) {
|
||||
try(Reader reader = new FileReader(walletFile, StandardCharsets.UTF_8)) {
|
||||
int firstChar = reader.read();
|
||||
|
||||
if(firstChar == 'U' || firstChar == '{') {
|
||||
@@ -511,6 +534,11 @@ public class Storage {
|
||||
private final Storage storage;
|
||||
private final SecureString password;
|
||||
|
||||
public LoadWalletService(Storage storage) {
|
||||
this.storage = storage;
|
||||
this.password = null;
|
||||
}
|
||||
|
||||
public LoadWalletService(Storage storage, SecureString password) {
|
||||
this.storage = storage;
|
||||
this.password = password;
|
||||
@@ -520,8 +548,15 @@ public class Storage {
|
||||
protected Task<WalletBackupAndKey> createTask() {
|
||||
return new Task<>() {
|
||||
protected WalletBackupAndKey call() throws IOException, StorageException {
|
||||
WalletBackupAndKey walletBackupAndKey = storage.loadEncryptedWallet(password);
|
||||
password.clear();
|
||||
WalletBackupAndKey walletBackupAndKey;
|
||||
|
||||
if(password != null) {
|
||||
walletBackupAndKey = storage.loadEncryptedWallet(password);
|
||||
password.clear();
|
||||
} else {
|
||||
walletBackupAndKey = storage.loadUnencryptedWallet();
|
||||
}
|
||||
|
||||
return walletBackupAndKey;
|
||||
}
|
||||
};
|
||||
@@ -531,10 +566,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
|
||||
@@ -542,7 +585,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();
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@ public class WalletBackupAndKey implements Comparable<WalletBackupAndKey> {
|
||||
|
||||
@Override
|
||||
public int compareTo(WalletBackupAndKey other) {
|
||||
if(wallet.getStandardAccountType() != null && other.wallet.getStandardAccountType() != null) {
|
||||
return wallet.getStandardAccountType().ordinal() - other.wallet.getStandardAccountType().ordinal();
|
||||
}
|
||||
|
||||
return wallet.getAccountIndex() - other.wallet.getAccountIndex();
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.sparrowwallet.sparrow.wallet.*;
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import com.zaxxer.hikari.pool.HikariPool;
|
||||
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
||||
import org.flywaydb.core.Flyway;
|
||||
import org.flywaydb.core.api.FlywayException;
|
||||
import org.flywaydb.core.api.exception.FlywayValidateException;
|
||||
@@ -33,6 +34,9 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -55,6 +59,7 @@ public class DbPersistence implements Persistence {
|
||||
|
||||
private Wallet masterWallet;
|
||||
private final Map<Wallet, DirtyPersistables> dirtyPersistablesMap = new HashMap<>();
|
||||
private ExecutorService updateExecutor;
|
||||
|
||||
public DbPersistence() {
|
||||
EventManager.get().register(this);
|
||||
@@ -88,11 +93,14 @@ 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<WalletBackupAndKey, Storage> childWallets = loadChildWallets(storage, masterWallet, backupWallet, encryptionKey);
|
||||
masterWallet.setChildWallets(childWallets.keySet().stream().map(WalletBackupAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
createUpdateExecutor(masterWallet);
|
||||
|
||||
return new WalletBackupAndKey(masterWallet, backupWallet, encryptionKey, keyDeriver, childWallets);
|
||||
}
|
||||
|
||||
@@ -156,7 +164,21 @@ public class DbPersistence implements Persistence {
|
||||
@Override
|
||||
public void updateWallet(Storage storage, Wallet wallet, ECKey encryptionPubKey) throws StorageException {
|
||||
updatePassword(storage, encryptionPubKey);
|
||||
update(storage, wallet, getFilePassword(encryptionPubKey));
|
||||
|
||||
updateExecutor.execute(() -> {
|
||||
try {
|
||||
update(storage, wallet, getFilePassword(encryptionPubKey));
|
||||
} catch(Exception e) {
|
||||
log.error("Error updating wallet db", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private synchronized void createUpdateExecutor(Wallet masterWallet) {
|
||||
if(updateExecutor == null) {
|
||||
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern(masterWallet.getFullName() + "-dbupdater").daemon(false).priority(Thread.NORM_PRIORITY).build();
|
||||
updateExecutor = Executors.newSingleThreadExecutor(factory);
|
||||
}
|
||||
}
|
||||
|
||||
private File renameToDbFile(File walletFile) throws IOException {
|
||||
@@ -195,6 +217,11 @@ public class DbPersistence implements Persistence {
|
||||
jdbi.useHandle(handle -> {
|
||||
WalletDao walletDao = handle.attach(WalletDao.class);
|
||||
try {
|
||||
if(dirtyPersistables.deleteAccount && !wallet.isMasterWallet()) {
|
||||
handle.execute("drop schema `" + getSchema(wallet) + "` cascade");
|
||||
return;
|
||||
}
|
||||
|
||||
walletDao.setSchema(getSchema(wallet));
|
||||
|
||||
if(dirtyPersistables.clearHistory) {
|
||||
@@ -235,10 +262,22 @@ 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.watchLast != null) {
|
||||
walletDao.updateWatchLast(wallet.getId(), dirtyPersistables.watchLast);
|
||||
}
|
||||
|
||||
if(!dirtyPersistables.labelEntries.isEmpty()) {
|
||||
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class);
|
||||
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class);
|
||||
@@ -323,6 +362,7 @@ public class DbPersistence implements Persistence {
|
||||
|
||||
if(wallet.isMasterWallet()) {
|
||||
masterWallet = wallet;
|
||||
createUpdateExecutor(masterWallet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,6 +575,24 @@ public class DbPersistence implements Persistence {
|
||||
@Override
|
||||
public void close() {
|
||||
EventManager.get().unregister(this);
|
||||
if(updateExecutor != null) {
|
||||
updateExecutor.shutdown();
|
||||
try {
|
||||
if(!updateExecutor.awaitTermination(1, TimeUnit.MINUTES)) {
|
||||
updateExecutor.shutdownNow();
|
||||
}
|
||||
|
||||
closeDataSource();
|
||||
} catch (InterruptedException e) {
|
||||
updateExecutor.shutdownNow();
|
||||
closeDataSource();
|
||||
}
|
||||
} else {
|
||||
closeDataSource();
|
||||
}
|
||||
}
|
||||
|
||||
private void closeDataSource() {
|
||||
if(dataSource != null && !dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
@@ -622,74 +680,108 @@ public class DbPersistence implements Persistence {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletDeleted(WalletDeletedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).deleteAccount = true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryCleared(WalletHistoryClearedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).clearHistory = true;
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).clearHistory = true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).historyNodes.addAll(event.getHistoryChangedNodes());
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).historyNodes.addAll(event.getHistoryChangedNodes()));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletLabelChanged(WalletLabelChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).label = event.getLabel());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletBlockHeightChanged(WalletBlockHeightChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).blockHeight = event.getBlockHeight();
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).blockHeight = event.getBlockHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletGapLimitChanged(WalletGapLimitChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).gapLimit = event.getGapLimit());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletEntryLabelsChanged(WalletEntryLabelsChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).labelEntries.addAll(event.getEntries());
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).labelEntries.addAll(event.getEntries()));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).utxoStatuses.addAll(event.getUtxos());
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).utxoStatuses.addAll(event.getUtxos()));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletMixConfigChanged(WalletMixConfigChangedEvent event) {
|
||||
if(persistsFor(event.getWallet()) && event.getWallet().getMixConfig() != null) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).mixConfig = true;
|
||||
updateExecutor.execute(() -> 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());
|
||||
updateExecutor.execute(() -> {
|
||||
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())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).labelKeystores.addAll(event.getChangedKeystores());
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).labelKeystores.addAll(event.getChangedKeystores()));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void keystoreEncryptionChanged(KeystoreEncryptionChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).encryptionKeystores.addAll(event.getChangedKeystores());
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).encryptionKeystores.addAll(event.getChangedKeystores()));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletWatchLastChanged(WalletWatchLastChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).watchLast = event.getWatchLast());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DirtyPersistables {
|
||||
public boolean deleteAccount;
|
||||
public boolean clearHistory;
|
||||
public final List<WalletNode> historyNodes = new ArrayList<>();
|
||||
public String label;
|
||||
public Integer blockHeight = null;
|
||||
public Integer gapLimit = null;
|
||||
public Integer watchLast = null;
|
||||
public final List<Entry> labelEntries = new ArrayList<>();
|
||||
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>();
|
||||
public boolean mixConfig;
|
||||
@@ -700,9 +792,13 @@ public class DbPersistence implements Persistence {
|
||||
|
||||
public String toString() {
|
||||
return "Dirty Persistables" +
|
||||
"\nDelete account:" + deleteAccount +
|
||||
"\nClear history:" + clearHistory +
|
||||
"\nNodes:" + historyNodes +
|
||||
"\nLabel:" + label +
|
||||
"\nBlockHeight:" + blockHeight +
|
||||
"\nGap limit:" + gapLimit +
|
||||
"\nWatch last:" + watchLast +
|
||||
"\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()) +
|
||||
|
||||
@@ -8,16 +8,16 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
|
||||
public interface MixConfigDao {
|
||||
@SqlQuery("select id, scode, mixOnStartup, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex from mixConfig where wallet = ?")
|
||||
@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, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex, wallet) values (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@SqlUpdate("insert into mixConfig (scode, mixOnStartup, indexRange, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex, wallet) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insertMixConfig(String scode, Boolean mixOnStartup, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet);
|
||||
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 = ?, mixToWalletFile = ?, mixToWalletName = ?, minMixes = ?, receiveIndex = ?, changeIndex = ?, wallet = ? where id = ?")
|
||||
void updateMixConfig(String scode, Boolean mixOnStartup, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet, long id);
|
||||
@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) {
|
||||
@@ -32,10 +32,10 @@ public interface MixConfigDao {
|
||||
}
|
||||
|
||||
if(mixConfig.getId() == null) {
|
||||
long id = insertMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId());
|
||||
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(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId(), mixConfig.getId());
|
||||
updateMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixConfig.getIndexRange(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId(), mixConfig.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user