mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-30 19:46:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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`
|
||||
|
||||
|
||||
+39
-16
@@ -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.5'
|
||||
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.202')
|
||||
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.6-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.25')
|
||||
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')
|
||||
}
|
||||
|
||||
@@ -142,7 +147,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 +196,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")
|
||||
@@ -206,12 +212,12 @@ jlink {
|
||||
imageOptions = []
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/aopp.properties', '--license-file', 'LICENSE']
|
||||
if(os.windows) {
|
||||
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 +360,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 +437,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 +452,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 +460,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.25.jar', 'com.sparrowwallet.nightjar', '0.2.25') {
|
||||
requires('com.google.common')
|
||||
requires('net.sourceforge.streamsupport')
|
||||
requires('org.slf4j')
|
||||
@@ -459,6 +470,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 +478,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 +572,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...c1f6a1245e
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
cd `dirname $0`
|
||||
args="$*"
|
||||
args="${args%"${args##*[![:space:]]}"}"
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.5.0</string>
|
||||
<string>1.5.5</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -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,6 +27,10 @@ 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;
|
||||
@@ -35,6 +41,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 +50,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 +61,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 +73,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 +93,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 +131,46 @@ 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 refreshWallet;
|
||||
|
||||
@FXML
|
||||
private MenuItem sendToMany;
|
||||
|
||||
@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 +187,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 +233,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 +255,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 +310,34 @@ 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);
|
||||
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
|
||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||
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());
|
||||
@@ -370,6 +423,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 +485,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 +614,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 +714,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 +801,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 +814,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);
|
||||
@@ -879,7 +961,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 +988,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 +1034,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 +1230,100 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
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 refreshWallet(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
@@ -1189,7 +1373,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 +1385,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 +1419,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 +1433,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 +1471,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 +1506,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 +1669,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 +1702,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 +1765,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 +1791,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 +1823,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 +1893,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);
|
||||
lockWallet.setDisable(walletTabData.getWalletForm().lockedProperty().get());
|
||||
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid());
|
||||
showLoadingLog.setDisable(false);
|
||||
showUtxosChart.setDisable(false);
|
||||
showTxHex.setDisable(true);
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1702,6 +1932,7 @@ public class AppController implements Initializable {
|
||||
if(selectedWalletForm != null) {
|
||||
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
||||
exportWallet.setDisable(!event.getWallet().isValid());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1709,8 +1940,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 +2062,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 +2191,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);
|
||||
@@ -2167,4 +2419,20 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletUnlock(WalletUnlockEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
|
||||
lockWallet.setDisable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.*;
|
||||
@@ -76,6 +80,8 @@ public class AppServices {
|
||||
|
||||
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,6 +100,8 @@ public class AppServices {
|
||||
|
||||
private TorService torService;
|
||||
|
||||
private ScheduledService<Void> preventSleepService;
|
||||
|
||||
private static Integer currentBlockHeight;
|
||||
|
||||
private static BlockHeader latestBlockHeader;
|
||||
@@ -148,6 +156,7 @@ public class AppServices {
|
||||
this.application = application;
|
||||
EventManager.get().register(this);
|
||||
EventManager.get().register(whirlpoolServices);
|
||||
EventManager.get().register(sorobanServices);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -156,6 +165,7 @@ public class AppServices {
|
||||
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
|
||||
versionCheckService = createVersionCheckService();
|
||||
torService = createTorService();
|
||||
preventSleepService = createPreventSleepService();
|
||||
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
|
||||
@@ -183,6 +193,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 +396,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 +477,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 +492,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 +545,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);
|
||||
@@ -581,7 +644,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 +717,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);
|
||||
@@ -803,6 +868,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.5";
|
||||
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) {}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +106,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
setText(address.toString());
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(nodeEntry.getNode().getDerivationPath().replace("m", ".."));
|
||||
tooltip.setText(nodeEntry.getNode().toString());
|
||||
setTooltip(tooltip);
|
||||
getStyleClass().add("address-cell");
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -190,7 +202,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);
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
long previousFeeRate = 0;
|
||||
for(Long feeRate : SendController.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()) {
|
||||
|
||||
@@ -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,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;
|
||||
@@ -491,6 +493,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(outputDescriptor.getKeyDerivation(extendedKey).getDerivation())));
|
||||
keystore.setExtendedPublicKey(extendedKey);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(outputDescriptor.isCosigner() ? PolicyType.MULTI : PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), 1));
|
||||
wallets.add(wallet);
|
||||
}
|
||||
|
||||
@@ -499,7 +502,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 +520,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);
|
||||
|
||||
@@ -3,16 +3,21 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||
import com.sparrowwallet.sparrow.event.SorobanInitiatedEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
@@ -22,6 +27,7 @@ import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.CubicCurve;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.util.Duration;
|
||||
@@ -32,12 +38,18 @@ import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TransactionDiagram extends GridPane {
|
||||
private static final int MAX_UTXOS = 7;
|
||||
private static final int MAX_PAYMENTS = 5;
|
||||
private static final double DIAGRAM_HEIGHT = 215.0;
|
||||
private static final int MAX_UTXOS = 8;
|
||||
private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 2;
|
||||
private static final int MAX_PAYMENTS = 6;
|
||||
private static final int REDUCED_MAX_PAYMENTS = MAX_PAYMENTS - 2;
|
||||
private static final double DIAGRAM_HEIGHT = 210.0;
|
||||
private static final double REDUCED_DIAGRAM_HEIGHT = DIAGRAM_HEIGHT - 60;
|
||||
private static final int TOOLTIP_SHOW_DELAY = 50;
|
||||
private static final int RELATIVE_SIZE_MAX_RADIUS = 7;
|
||||
|
||||
private WalletTransaction walletTx;
|
||||
private final BooleanProperty finalProperty = new SimpleBooleanProperty(false);
|
||||
private final ObjectProperty<OptimizationStrategy> optimizationStrategyProperty = new SimpleObjectProperty<>(OptimizationStrategy.EFFICIENCY);
|
||||
|
||||
public void update(WalletTransaction walletTx) {
|
||||
setMinHeight(getDiagramHeight());
|
||||
@@ -76,15 +88,15 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
public void update() {
|
||||
Map<BlockTransactionHashIndex, WalletNode> displayedUtxos = getDisplayedUtxos();
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = getDisplayedUtxoSets();
|
||||
|
||||
Pane inputsTypePane = getInputsType(displayedUtxos);
|
||||
Pane inputsTypePane = getInputsType(displayedUtxoSets);
|
||||
GridPane.setConstraints(inputsTypePane, 0, 0);
|
||||
|
||||
Pane inputsPane = getInputsLabels(displayedUtxos);
|
||||
Pane inputsPane = getInputsLabels(displayedUtxoSets);
|
||||
GridPane.setConstraints(inputsPane, 1, 0);
|
||||
|
||||
Node inputsLinesPane = getInputsLines(displayedUtxos);
|
||||
Node inputsLinesPane = getInputsLines(displayedUtxoSets);
|
||||
GridPane.setConstraints(inputsLinesPane, 2, 0);
|
||||
|
||||
Pane txPane = getTransactionPane();
|
||||
@@ -102,19 +114,57 @@ public class TransactionDiagram extends GridPane {
|
||||
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
||||
}
|
||||
|
||||
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() {
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos();
|
||||
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
|
||||
boolean addUserSet = getOptimizationStrategy() == OptimizationStrategy.PRIVACY && SorobanServices.canWalletMix(walletTx.getWallet())
|
||||
&& walletTx.getPayments().size() == 1
|
||||
&& (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getAddress(walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE)).getScriptType());
|
||||
|
||||
if(getPayjoinURI() != null) {
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>();
|
||||
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) {
|
||||
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, addUserSet ? 2 : walletTx.getSelectedUtxoSets().size()));
|
||||
}
|
||||
|
||||
if(addUserSet && displayedUtxoSets.size() == 1) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> addUserUtxoSet = new HashMap<>();
|
||||
addUserUtxoSet.put(new AddUserBlockTransactionHashIndex(!walletTx.isTwoPersonCoinjoin()), null);
|
||||
displayedUtxoSets.add(addUserUtxoSet);
|
||||
}
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> paddedUtxoSets = new ArrayList<>();
|
||||
int maxDisplayedSetSize = displayedUtxoSets.stream().mapToInt(Map::size).max().orElse(0);
|
||||
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : displayedUtxoSets) {
|
||||
int toAdd = maxDisplayedSetSize - selectedUtxoSet.size();
|
||||
if(toAdd > 0) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> paddedUtxoSet = new LinkedHashMap<>();
|
||||
int firstAdd = toAdd / 2;
|
||||
for(int i = 0; i < firstAdd; i++) {
|
||||
paddedUtxoSet.put(new InvisibleBlockTransactionHashIndex(i), null);
|
||||
}
|
||||
paddedUtxoSet.putAll(selectedUtxoSet);
|
||||
for(int i = firstAdd; i < toAdd; i++) {
|
||||
paddedUtxoSet.put(new InvisibleBlockTransactionHashIndex(i), null);
|
||||
}
|
||||
paddedUtxoSets.add(paddedUtxoSet);
|
||||
} else {
|
||||
paddedUtxoSets.add(selectedUtxoSet);
|
||||
}
|
||||
}
|
||||
|
||||
return paddedUtxoSets;
|
||||
}
|
||||
|
||||
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos(Map<BlockTransactionHashIndex, WalletNode> selectedUtxos, int numSets) {
|
||||
if(getPayjoinURI() != null && !selectedUtxos.containsValue(null)) {
|
||||
selectedUtxos = new LinkedHashMap<>(selectedUtxos);
|
||||
selectedUtxos.put(new PayjoinBlockTransactionHashIndex(), null);
|
||||
}
|
||||
|
||||
if(selectedUtxos.size() > MAX_UTXOS) {
|
||||
int maxUtxosPerSet = getMaxUtxos() / numSets;
|
||||
if(selectedUtxos.size() > maxUtxosPerSet) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> utxos = new LinkedHashMap<>();
|
||||
List<BlockTransactionHashIndex> additional = new ArrayList<>();
|
||||
for(BlockTransactionHashIndex reference : selectedUtxos.keySet()) {
|
||||
if(utxos.size() < MAX_UTXOS - 1) {
|
||||
if(utxos.size() < maxUtxosPerSet - 1) {
|
||||
utxos.put(reference, selectedUtxos.get(reference));
|
||||
} else {
|
||||
additional.add(reference);
|
||||
@@ -144,62 +194,98 @@ public class TransactionDiagram extends GridPane {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Pane getInputsType(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) {
|
||||
StackPane stackPane = new StackPane();
|
||||
private Pane getInputsType(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
||||
double width = 22.0;
|
||||
double height = getDiagramHeight() - 10;
|
||||
|
||||
if(walletTx.isCoinControlUsed()) {
|
||||
VBox pane = new VBox();
|
||||
double width = 22.0;
|
||||
Group group = new Group();
|
||||
VBox.setVgrow(group, Priority.ALWAYS);
|
||||
VBox allBrackets = new VBox(10);
|
||||
allBrackets.setPrefWidth(width);
|
||||
allBrackets.setPadding(new Insets(5, 0, 5, 0));
|
||||
allBrackets.setAlignment(Pos.CENTER);
|
||||
|
||||
Line widthLine = new Line();
|
||||
widthLine.setStartX(0);
|
||||
widthLine.setEndX(width);
|
||||
widthLine.getStyleClass().add("boundary");
|
||||
|
||||
Line topYaxis = new Line();
|
||||
topYaxis.setStartX(width * 0.5);
|
||||
topYaxis.setStartY(getDiagramHeight() * 0.5 - 20.0);
|
||||
topYaxis.setEndX(width * 0.5);
|
||||
topYaxis.setEndY(10);
|
||||
topYaxis.getStyleClass().add("inputs-type");
|
||||
|
||||
Line topBracket = new Line();
|
||||
topBracket.setStartX(width * 0.5);
|
||||
topBracket.setStartY(10);
|
||||
topBracket.setEndX(width);
|
||||
topBracket.setEndY(10);
|
||||
topBracket.getStyleClass().add("inputs-type");
|
||||
|
||||
Line bottomYaxis = new Line();
|
||||
bottomYaxis.setStartX(width * 0.5);
|
||||
bottomYaxis.setStartY(getDiagramHeight() - 10);
|
||||
bottomYaxis.setEndX(width * 0.5);
|
||||
bottomYaxis.setEndY(getDiagramHeight() * 0.5 + 20.0);
|
||||
bottomYaxis.getStyleClass().add("inputs-type");
|
||||
|
||||
Line bottomBracket = new Line();
|
||||
bottomBracket.setStartX(width * 0.5);
|
||||
bottomBracket.setStartY(getDiagramHeight() - 10);
|
||||
bottomBracket.setEndX(width);
|
||||
bottomBracket.setEndY(getDiagramHeight() - 10);
|
||||
bottomBracket.getStyleClass().add("inputs-type");
|
||||
|
||||
group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket);
|
||||
pane.getChildren().add(group);
|
||||
|
||||
Glyph lockGlyph = getLockGlyph();
|
||||
lockGlyph.getStyleClass().add("inputs-type");
|
||||
Tooltip tooltip = new Tooltip("Coin control active");
|
||||
lockGlyph.setTooltip(tooltip);
|
||||
stackPane.getChildren().addAll(pane, lockGlyph);
|
||||
int numSets = displayedUtxoSets.size();
|
||||
if(numSets > 1) {
|
||||
double setHeight = (height / numSets) - 5;
|
||||
for(int set = 0; set < numSets; set++) {
|
||||
boolean externalUserSet = displayedUtxoSets.get(set).values().stream().anyMatch(Objects::nonNull);
|
||||
boolean addUserSet = displayedUtxoSets.get(set).keySet().stream().anyMatch(ref -> ref instanceof AddUserBlockTransactionHashIndex);
|
||||
if(externalUserSet || addUserSet) {
|
||||
boolean replace = !isFinal() && set > 0 && SorobanServices.canWalletMix(walletTx.getWallet());
|
||||
Glyph bracketGlyph = !replace && walletTx.isCoinControlUsed() ? getLockGlyph() : (addUserSet ? getUserAddGlyph() : getCoinsGlyph(replace));
|
||||
String tooltipText = addUserSet ? "Click to add a mix partner" : (walletTx.getWallet().getFullDisplayName() + (replace ? "\nClick to replace with a mix partner" : ""));
|
||||
StackPane stackPane = getBracket(width, setHeight, bracketGlyph, tooltipText);
|
||||
allBrackets.getChildren().add(stackPane);
|
||||
} else {
|
||||
StackPane stackPane = getBracket(width, setHeight, getUserGlyph(), "Mix partner");
|
||||
allBrackets.getChildren().add(stackPane);
|
||||
}
|
||||
}
|
||||
} else if(walletTx.isCoinControlUsed()) {
|
||||
StackPane stackPane = getBracket(width, height, getLockGlyph(), "Coin control active");
|
||||
allBrackets.getChildren().add(stackPane);
|
||||
}
|
||||
|
||||
return allBrackets;
|
||||
}
|
||||
|
||||
private StackPane getBracket(double width, double height, Glyph glyph, String tooltipText) {
|
||||
StackPane stackPane = new StackPane();
|
||||
VBox pane = new VBox();
|
||||
|
||||
Group group = new Group();
|
||||
VBox.setVgrow(group, Priority.ALWAYS);
|
||||
|
||||
int padding = 0;
|
||||
double iconPadding = 20.0;
|
||||
|
||||
Line widthLine = new Line();
|
||||
widthLine.setStartX(0);
|
||||
widthLine.setEndX(width);
|
||||
widthLine.getStyleClass().add("boundary");
|
||||
|
||||
Line topYaxis = new Line();
|
||||
topYaxis.setStartX(width * 0.5);
|
||||
topYaxis.setStartY(height * 0.5 - iconPadding);
|
||||
topYaxis.setEndX(width * 0.5);
|
||||
topYaxis.setEndY(padding);
|
||||
topYaxis.getStyleClass().add("inputs-type");
|
||||
|
||||
Line topBracket = new Line();
|
||||
topBracket.setStartX(width * 0.5);
|
||||
topBracket.setStartY(padding);
|
||||
topBracket.setEndX(width);
|
||||
topBracket.setEndY(padding);
|
||||
topBracket.getStyleClass().add("inputs-type");
|
||||
|
||||
Line bottomYaxis = new Line();
|
||||
bottomYaxis.setStartX(width * 0.5);
|
||||
bottomYaxis.setStartY(height - padding);
|
||||
bottomYaxis.setEndX(width * 0.5);
|
||||
bottomYaxis.setEndY(height * 0.5 + iconPadding);
|
||||
bottomYaxis.getStyleClass().add("inputs-type");
|
||||
|
||||
Line bottomBracket = new Line();
|
||||
bottomBracket.setStartX(width * 0.5);
|
||||
bottomBracket.setStartY(height - padding);
|
||||
bottomBracket.setEndX(width);
|
||||
bottomBracket.setEndY(height - padding);
|
||||
bottomBracket.getStyleClass().add("inputs-type");
|
||||
|
||||
group.getChildren().addAll(widthLine, topYaxis, topBracket, bottomYaxis, bottomBracket);
|
||||
pane.getChildren().add(group);
|
||||
|
||||
glyph.getStyleClass().add("inputs-type");
|
||||
Tooltip tooltip = new Tooltip(tooltipText);
|
||||
tooltip.getStyleClass().add("transaction-tooltip");
|
||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
tooltip.setShowDuration(Duration.INDEFINITE);
|
||||
glyph.setTooltip(tooltip);
|
||||
stackPane.getChildren().addAll(pane, glyph);
|
||||
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
private Pane getInputsLabels(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) {
|
||||
private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
||||
VBox inputsBox = new VBox();
|
||||
inputsBox.setMaxWidth(150);
|
||||
inputsBox.setPrefWidth(150);
|
||||
@@ -207,47 +293,71 @@ public class TransactionDiagram extends GridPane {
|
||||
inputsBox.minHeightProperty().bind(minHeightProperty());
|
||||
inputsBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
inputsBox.getChildren().add(createSpacer());
|
||||
for(BlockTransactionHashIndex input : displayedUtxos.keySet()) {
|
||||
WalletNode walletNode = displayedUtxos.get(input);
|
||||
String desc = getInputDescription(input);
|
||||
Label label = new Label(desc);
|
||||
label.getStyleClass().add("utxo-label");
|
||||
for(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos : displayedUtxoSets) {
|
||||
for(BlockTransactionHashIndex input : displayedUtxos.keySet()) {
|
||||
WalletNode walletNode = displayedUtxos.get(input);
|
||||
String desc = getInputDescription(input);
|
||||
Label label = new Label(desc);
|
||||
label.getStyleClass().add("utxo-label");
|
||||
|
||||
Button excludeUtxoButton = new Button("");
|
||||
excludeUtxoButton.setGraphic(getExcludeGlyph());
|
||||
excludeUtxoButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ExcludeUtxoEvent(walletTx, input));
|
||||
});
|
||||
Button excludeUtxoButton = new Button("");
|
||||
excludeUtxoButton.setGraphic(getExcludeGlyph());
|
||||
excludeUtxoButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ExcludeUtxoEvent(walletTx, input));
|
||||
});
|
||||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
if(walletNode != null) {
|
||||
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + walletNode.getDerivationPath().replace("m", "..") + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
Tooltip tooltip = new Tooltip();
|
||||
if(walletNode != null) {
|
||||
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + (isFinal() ? walletTx.getWallet().getFullDisplayName() : "") + " " + walletNode + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
|
||||
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
||||
label.getStyleClass().add("input-label");
|
||||
}
|
||||
|
||||
label.setGraphic(excludeUtxoButton);
|
||||
label.setContentDisplay(ContentDisplay.LEFT);
|
||||
} else {
|
||||
if(input instanceof PayjoinBlockTransactionHashIndex) {
|
||||
tooltip.setText("Added once transaction is signed and sent to the payjoin server");
|
||||
} else {
|
||||
AdditionalBlockTransactionHashIndex additionalReference = (AdditionalBlockTransactionHashIndex) input;
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
|
||||
joiner.add(getInputDescription(additionalInput));
|
||||
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
||||
label.getStyleClass().add("input-label");
|
||||
}
|
||||
tooltip.setText(joiner.toString());
|
||||
}
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
}
|
||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
label.setTooltip(tooltip);
|
||||
|
||||
inputsBox.getChildren().add(label);
|
||||
inputsBox.getChildren().add(createSpacer());
|
||||
if(!isFinal()) {
|
||||
label.setGraphic(excludeUtxoButton);
|
||||
label.setContentDisplay(ContentDisplay.LEFT);
|
||||
}
|
||||
} else {
|
||||
if(input instanceof PayjoinBlockTransactionHashIndex) {
|
||||
tooltip.setText("Added once transaction is signed and sent to the payjoin server");
|
||||
} else if(input instanceof AdditionalBlockTransactionHashIndex additionalReference) {
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
|
||||
joiner.add(getInputDescription(additionalInput));
|
||||
}
|
||||
tooltip.setText(joiner.toString());
|
||||
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
|
||||
tooltip.setText("");
|
||||
} else if(input instanceof AddUserBlockTransactionHashIndex) {
|
||||
tooltip.setText("");
|
||||
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getWarningGlyph());
|
||||
label.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
});
|
||||
} else {
|
||||
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
|
||||
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int) input.getIndex());
|
||||
Address fromAddress = txOutput.getScript().getToAddress();
|
||||
tooltip.setText("Input of " + getSatsValue(txOutput.getValue()) + " sats\n" + input.getHashAsString() + ":" + input.getIndex() + (fromAddress != null ? "\n" + fromAddress : ""));
|
||||
} else {
|
||||
tooltip.setText(input.getHashAsString() + ":" + input.getIndex());
|
||||
}
|
||||
label.getStyleClass().add("input-label");
|
||||
}
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
}
|
||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
tooltip.setShowDuration(Duration.INDEFINITE);
|
||||
if(!tooltip.getText().isEmpty()) {
|
||||
label.setTooltip(tooltip);
|
||||
}
|
||||
|
||||
inputsBox.getChildren().add(label);
|
||||
inputsBox.getChildren().add(createSpacer());
|
||||
}
|
||||
}
|
||||
|
||||
return inputsBox;
|
||||
@@ -261,7 +371,10 @@ public class TransactionDiagram extends GridPane {
|
||||
return String.format(Locale.ENGLISH, "%,d", amount);
|
||||
}
|
||||
|
||||
private Pane getInputsLines(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos) {
|
||||
private Pane getInputsLines(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> displayedUtxos = new LinkedHashMap<>();
|
||||
displayedUtxoSets.forEach(displayedUtxos::putAll);
|
||||
|
||||
VBox pane = new VBox();
|
||||
Group group = new Group();
|
||||
VBox.setVgrow(group, Priority.ALWAYS);
|
||||
@@ -276,16 +389,19 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
double width = 140.0;
|
||||
List<BlockTransactionHashIndex> inputs = new ArrayList<>(displayedUtxos.keySet());
|
||||
long sum = walletTx.getTotal();
|
||||
int numUtxos = displayedUtxos.size();
|
||||
for(int i = 1; i <= numUtxos; i++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("input-line");
|
||||
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex) {
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex || inputs.get(numUtxos-i) instanceof AddUserBlockTransactionHashIndex) {
|
||||
curve.getStyleClass().add("input-dashed-line");
|
||||
} else if(inputs.get(numUtxos-i) instanceof InvisibleBlockTransactionHashIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
curve.setStartX(0);
|
||||
curve.setStartX(RELATIVE_SIZE_MAX_RADIUS);
|
||||
double scaleFactor = (double)i / (numUtxos + 1);
|
||||
int nodeHeight = 17;
|
||||
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
|
||||
@@ -293,12 +409,19 @@ public class TransactionDiagram extends GridPane {
|
||||
curve.setEndX(width);
|
||||
curve.setEndY(scale(getDiagramHeight(), 0.5, 0));
|
||||
|
||||
curve.setControlX1(scale(width, 0.2, 0));
|
||||
curve.setControlX1(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.2, 0));
|
||||
curve.setControlY1(curve.getStartY());
|
||||
curve.setControlX2(scale(width, 0.8, 0));
|
||||
curve.setControlX2(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.8, 0));
|
||||
curve.setControlY2(curve.getEndY());
|
||||
|
||||
group.getChildren().add(curve);
|
||||
|
||||
if(sum > 0 && !curve.getStyleClass().contains("input-dashed-line")) {
|
||||
long radius = Math.round((double)inputs.get(numUtxos-i).getValue() * (RELATIVE_SIZE_MAX_RADIUS - 1) / sum) + 1;
|
||||
Circle circle = new Circle(curve.getStartX(), curve.getStartY(), radius);
|
||||
circle.getStyleClass().add("size-indicator");
|
||||
group.getChildren().add(circle);
|
||||
}
|
||||
}
|
||||
|
||||
pane.getChildren().add(group);
|
||||
@@ -312,11 +435,12 @@ public class TransactionDiagram extends GridPane {
|
||||
private List<Payment> getDisplayedPayments() {
|
||||
List<Payment> payments = walletTx.getPayments();
|
||||
|
||||
if(payments.size() > MAX_PAYMENTS) {
|
||||
int maxPayments = getMaxPayments();
|
||||
if(payments.size() > maxPayments) {
|
||||
List<Payment> displayedPayments = new ArrayList<>();
|
||||
List<Payment> additional = new ArrayList<>();
|
||||
for(Payment payment : payments) {
|
||||
if(displayedPayments.size() < MAX_PAYMENTS - 1) {
|
||||
if(displayedPayments.size() < maxPayments - 1) {
|
||||
displayedPayments.add(payment);
|
||||
} else {
|
||||
additional.add(payment);
|
||||
@@ -344,6 +468,9 @@ public class TransactionDiagram extends GridPane {
|
||||
group.getChildren().add(yaxisLine);
|
||||
|
||||
double width = 140.0;
|
||||
long sum = walletTx.getTotal();
|
||||
List<Long> values = walletTx.getTransaction().getOutputs().stream().map(TransactionOutput::getValue).collect(Collectors.toList());
|
||||
values.add(walletTx.getFee());
|
||||
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
||||
for(int i = 1; i <= numOutputs; i++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
@@ -351,18 +478,25 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
curve.setStartX(0);
|
||||
curve.setStartY(scale(getDiagramHeight(), 0.5, 0));
|
||||
curve.setEndX(width);
|
||||
curve.setEndX(width - RELATIVE_SIZE_MAX_RADIUS);
|
||||
double scaleFactor = (double)i / (numOutputs + 1);
|
||||
int nodeHeight = 20;
|
||||
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
|
||||
curve.setEndY(scale(getDiagramHeight(), scaleFactor, additional));
|
||||
|
||||
curve.setControlX1(scale(width, 0.2, 0));
|
||||
curve.setControlX1(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.2, 0));
|
||||
curve.controlY1Property().bind(curve.startYProperty());
|
||||
curve.setControlX2(scale(width, 0.8, 0));
|
||||
curve.setControlX2(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.8, 0));
|
||||
curve.controlY2Property().bind(curve.endYProperty());
|
||||
|
||||
group.getChildren().add(curve);
|
||||
|
||||
if(sum > 0) {
|
||||
long radius = Math.min(RELATIVE_SIZE_MAX_RADIUS, Math.round((double)values.get(numOutputs-i) * (RELATIVE_SIZE_MAX_RADIUS - 1) / sum) + 1);
|
||||
Circle circle = new Circle(curve.getEndX(), curve.getEndY(), radius);
|
||||
circle.getStyleClass().add("size-indicator");
|
||||
group.getChildren().add(circle);
|
||||
}
|
||||
}
|
||||
|
||||
pane.getChildren().add(group);
|
||||
@@ -376,19 +510,24 @@ public class TransactionDiagram extends GridPane {
|
||||
outputsBox.setAlignment(Pos.CENTER_LEFT);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
List<OutputNode> outputNodes = new ArrayList<>();
|
||||
for(Payment payment : displayedPayments) {
|
||||
Glyph outputGlyph = getOutputGlyph(payment);
|
||||
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
|
||||
String recipientDesc = labelledPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
|
||||
Label recipientLabel = new Label(recipientDesc, outputGlyph);
|
||||
payment.setLabel(getOutputLabel(payment));
|
||||
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX || payment.getType() == Payment.Type.MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
Tooltip recipientTooltip = new Tooltip((walletTx.isConsolidationSend(payment) ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString()));
|
||||
Wallet toWallet = getToWallet(payment);
|
||||
WalletNode toNode = walletTx.getWallet() != null ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null;
|
||||
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
||||
+ getSatsValue(payment.getAmount()) + " sats to "
|
||||
+ (payment instanceof AdditionalPayment ? "\n" + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : "external address") : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString()));
|
||||
recipientTooltip.getStyleClass().add("recipient-label");
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
recipientLabel.setTooltip(recipientTooltip);
|
||||
outputsBox.getChildren().add(recipientLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
outputNodes.add(new OutputNode(recipientLabel, payment.getAddress(), payment.getAmount()));
|
||||
}
|
||||
|
||||
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
|
||||
@@ -397,29 +536,42 @@ public class TransactionDiagram extends GridPane {
|
||||
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
String changeDesc = walletTx.getChangeAddress(changeNode).toString().substring(0, 8) + "...";
|
||||
Address changeAddress = walletTx.getChangeAddress(changeNode);
|
||||
String changeDesc = changeAddress.toString().substring(0, 8) + "...";
|
||||
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
|
||||
changeLabel.getStyleClass().addAll("output-label", "change-label");
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode.getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
|
||||
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
|
||||
changeTooltip.getStyleClass().add("change-label");
|
||||
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
changeTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
changeLabel.setTooltip(changeTooltip);
|
||||
actionBox.getChildren().add(changeLabel);
|
||||
|
||||
Button nextChangeAddressButton = new Button("");
|
||||
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
|
||||
nextChangeAddressButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
|
||||
});
|
||||
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
|
||||
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
|
||||
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
|
||||
replaceChangeLabel.getStyleClass().add("replace-change-label");
|
||||
replaceChangeLabel.setVisible(false);
|
||||
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
|
||||
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
|
||||
if(!isFinal()) {
|
||||
Button nextChangeAddressButton = new Button("");
|
||||
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
|
||||
nextChangeAddressButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
|
||||
});
|
||||
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
|
||||
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
|
||||
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
|
||||
replaceChangeLabel.getStyleClass().add("replace-change-label");
|
||||
replaceChangeLabel.setVisible(false);
|
||||
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
|
||||
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
|
||||
actionBox.getChildren().add(replaceChangeLabel);
|
||||
}
|
||||
|
||||
actionBox.getChildren().addAll(changeLabel, replaceChangeLabel);
|
||||
outputsBox.getChildren().add(actionBox);
|
||||
outputNodes.add(new OutputNode(actionBox, changeAddress, changeEntry.getValue()));
|
||||
}
|
||||
|
||||
if(isFinal()) {
|
||||
Collections.sort(outputNodes);
|
||||
}
|
||||
|
||||
for(OutputNode outputNode : outputNodes) {
|
||||
outputsBox.getChildren().add(outputNode.outputLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
}
|
||||
|
||||
@@ -427,9 +579,10 @@ public class TransactionDiagram extends GridPane {
|
||||
Label feeLabel = highFee ? new Label("High Fee", getWarningGlyph()) : new Label("Fee", getFeeGlyph());
|
||||
feeLabel.getStyleClass().addAll("output-label", "fee-label");
|
||||
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
|
||||
Tooltip feeTooltip = new Tooltip("Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
||||
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
|
||||
feeTooltip.getStyleClass().add("fee-tooltip");
|
||||
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
feeTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
feeLabel.setTooltip(feeTooltip);
|
||||
outputsBox.getChildren().add(feeLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
@@ -439,14 +592,18 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
private Pane getTransactionPane() {
|
||||
VBox txPane = new VBox();
|
||||
txPane.setPadding(new Insets(0, 10, 0, 10));
|
||||
txPane.setPadding(new Insets(0, 5, 0, 5));
|
||||
txPane.setAlignment(Pos.CENTER);
|
||||
txPane.getChildren().add(createSpacer());
|
||||
|
||||
String txDesc = "Transaction";
|
||||
Label txLabel = new Label(txDesc);
|
||||
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes");
|
||||
boolean isFinalized = walletTx.getTransaction().hasScriptSigs() || walletTx.getTransaction().hasWitnesses();
|
||||
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n"
|
||||
+ String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes"
|
||||
+ (walletTx.getFee() < 0 ? "" : "\n" + String.format("%.2f", walletTx.getFee() / walletTx.getTransaction().getVirtualSize()) + " sats/vB" + (isFinalized ? "" : " (non-final)")));
|
||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
tooltip.setShowDuration(Duration.INDEFINITE);
|
||||
tooltip.getStyleClass().add("transaction-tooltip");
|
||||
txLabel.setTooltip(tooltip);
|
||||
txPane.getChildren().add(txLabel);
|
||||
@@ -456,21 +613,74 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
public double getDiagramHeight() {
|
||||
if(AppServices.isReducedWindowHeight(this)) {
|
||||
return DIAGRAM_HEIGHT - 40;
|
||||
if(isReducedHeight()) {
|
||||
return REDUCED_DIAGRAM_HEIGHT;
|
||||
}
|
||||
|
||||
return DIAGRAM_HEIGHT;
|
||||
}
|
||||
|
||||
private int getMaxUtxos() {
|
||||
if(isReducedHeight()) {
|
||||
return REDUCED_MAX_UTXOS;
|
||||
}
|
||||
|
||||
return MAX_UTXOS;
|
||||
}
|
||||
|
||||
private int getMaxPayments() {
|
||||
if(isReducedHeight()) {
|
||||
return REDUCED_MAX_PAYMENTS;
|
||||
}
|
||||
|
||||
return MAX_PAYMENTS;
|
||||
}
|
||||
|
||||
private boolean isReducedHeight() {
|
||||
return !isFinal() && AppServices.isReducedWindowHeight(this);
|
||||
}
|
||||
|
||||
private Node createSpacer() {
|
||||
final Region spacer = new Region();
|
||||
VBox.setVgrow(spacer, Priority.ALWAYS);
|
||||
return spacer;
|
||||
}
|
||||
|
||||
private String getOutputLabel(Payment payment) {
|
||||
if(payment.getLabel() != null) {
|
||||
return payment.getLabel();
|
||||
}
|
||||
|
||||
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
|
||||
return "Whirlpool Fee";
|
||||
} else if(walletTx.isPremixSend(payment)) {
|
||||
int premixIndex = getOutputIndex(payment.getAddress(), payment.getAmount()) - 2;
|
||||
return "Premix #" + premixIndex;
|
||||
} else if(walletTx.isBadbankSend(payment)) {
|
||||
return "Badbank Change";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private int getOutputIndex(Address address, long amount) {
|
||||
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private Wallet getToWallet(Payment payment) {
|
||||
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
|
||||
if(openWallet != walletTx.getWallet() && openWallet.isValid() && openWallet.isWalletAddress(payment.getAddress())) {
|
||||
return openWallet;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Glyph getOutputGlyph(Payment payment) {
|
||||
if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
|
||||
if(payment.getType().equals(Payment.Type.MIX)) {
|
||||
return getMixGlyph();
|
||||
} else if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
|
||||
return getFakeMixGlyph();
|
||||
} else if(walletTx.isConsolidationSend(payment)) {
|
||||
return getConsolidationGlyph();
|
||||
@@ -482,6 +692,8 @@ public class TransactionDiagram extends GridPane {
|
||||
return getWhirlpoolFeeGlyph();
|
||||
} else if(payment instanceof AdditionalPayment) {
|
||||
return ((AdditionalPayment)payment).getOutputGlyph(this);
|
||||
} else if(getToWallet(payment) != null) {
|
||||
return getDepositGlyph();
|
||||
}
|
||||
|
||||
return getPaymentGlyph();
|
||||
@@ -508,6 +720,13 @@ public class TransactionDiagram extends GridPane {
|
||||
return consolidationGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getDepositGlyph() {
|
||||
Glyph depositGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||
depositGlyph.getStyleClass().add("deposit-icon");
|
||||
depositGlyph.setFontSize(12);
|
||||
return depositGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getPremixGlyph() {
|
||||
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
premixGlyph.getStyleClass().add("premix-icon");
|
||||
@@ -540,9 +759,9 @@ public class TransactionDiagram extends GridPane {
|
||||
return getChangeGlyph();
|
||||
}
|
||||
|
||||
public static Glyph getPayjoinGlyph() {
|
||||
public static Glyph getMixGlyph() {
|
||||
Glyph payjoinGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
payjoinGlyph.getStyleClass().add("payjoin-icon");
|
||||
payjoinGlyph.getStyleClass().add("mix-icon");
|
||||
payjoinGlyph.setFontSize(12);
|
||||
return payjoinGlyph;
|
||||
}
|
||||
@@ -582,6 +801,13 @@ public class TransactionDiagram extends GridPane {
|
||||
return feeWarningGlyph;
|
||||
}
|
||||
|
||||
private Glyph getQuestionGlyph() {
|
||||
Glyph feeWarningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QUESTION_CIRCLE);
|
||||
feeWarningGlyph.getStyleClass().add("question-icon");
|
||||
feeWarningGlyph.setFontSize(12);
|
||||
return feeWarningGlyph;
|
||||
}
|
||||
|
||||
private Glyph getLockGlyph() {
|
||||
Glyph lockGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LOCK);
|
||||
lockGlyph.getStyleClass().add("lock-icon");
|
||||
@@ -589,6 +815,76 @@ public class TransactionDiagram extends GridPane {
|
||||
return lockGlyph;
|
||||
}
|
||||
|
||||
private Glyph getUserGlyph() {
|
||||
Glyph userGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER);
|
||||
userGlyph.getStyleClass().add("user-icon");
|
||||
userGlyph.setFontSize(12);
|
||||
return userGlyph;
|
||||
}
|
||||
|
||||
private Glyph getUserAddGlyph() {
|
||||
Glyph userAddGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER_PLUS);
|
||||
userAddGlyph.getStyleClass().add("useradd-icon");
|
||||
userAddGlyph.setFontSize(12);
|
||||
userAddGlyph.setOnMouseEntered(event -> {
|
||||
userAddGlyph.setFontSize(18);
|
||||
});
|
||||
userAddGlyph.setOnMouseExited(event -> {
|
||||
userAddGlyph.setFontSize(12);
|
||||
});
|
||||
userAddGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
});
|
||||
return userAddGlyph;
|
||||
}
|
||||
|
||||
private Glyph getCoinsGlyph(boolean allowReplacement) {
|
||||
Glyph coinsGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
|
||||
coinsGlyph.setFontSize(12);
|
||||
if(allowReplacement) {
|
||||
coinsGlyph.getStyleClass().add("coins-replace-icon");
|
||||
coinsGlyph.setOnMouseEntered(event -> {
|
||||
coinsGlyph.setIcon(FontAwesome5.Glyph.USER_PLUS);
|
||||
coinsGlyph.setFontSize(18);
|
||||
});
|
||||
coinsGlyph.setOnMouseExited(event -> {
|
||||
coinsGlyph.setIcon(FontAwesome5.Glyph.COINS);
|
||||
coinsGlyph.setFontSize(12);
|
||||
});
|
||||
coinsGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
});
|
||||
} else {
|
||||
coinsGlyph.getStyleClass().add("coins-icon");
|
||||
}
|
||||
|
||||
return coinsGlyph;
|
||||
}
|
||||
|
||||
public boolean isFinal() {
|
||||
return finalProperty.get();
|
||||
}
|
||||
|
||||
public BooleanProperty finalProperty() {
|
||||
return finalProperty;
|
||||
}
|
||||
|
||||
public void setFinal(boolean isFinal) {
|
||||
this.finalProperty.set(isFinal);
|
||||
}
|
||||
|
||||
public OptimizationStrategy getOptimizationStrategy() {
|
||||
return optimizationStrategyProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<OptimizationStrategy> optimizationStrategyProperty() {
|
||||
return optimizationStrategyProperty;
|
||||
}
|
||||
|
||||
public void setOptimizationStrategy(OptimizationStrategy optimizationStrategy) {
|
||||
this.optimizationStrategyProperty.set(optimizationStrategy);
|
||||
}
|
||||
|
||||
private static class PayjoinBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
public PayjoinBlockTransactionHashIndex() {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
@@ -604,7 +900,7 @@ public class TransactionDiagram extends GridPane {
|
||||
private final List<BlockTransactionHashIndex> additionalInputs;
|
||||
|
||||
public AdditionalBlockTransactionHashIndex(List<BlockTransactionHashIndex> additionalInputs) {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, additionalInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum());
|
||||
this.additionalInputs = additionalInputs;
|
||||
}
|
||||
|
||||
@@ -618,6 +914,31 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
}
|
||||
|
||||
private static class InvisibleBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
public InvisibleBlockTransactionHashIndex(int index) {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, index, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return " ";
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddUserBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
private final boolean required;
|
||||
|
||||
public AddUserBlockTransactionHashIndex(boolean required) {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return "Add Mix Partner" + (required ? "" : "?");
|
||||
}
|
||||
}
|
||||
|
||||
private static class AdditionalPayment extends Payment {
|
||||
private final List<Payment> additionalPayments;
|
||||
|
||||
@@ -644,4 +965,25 @@ public class TransactionDiagram extends GridPane {
|
||||
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
private class OutputNode implements Comparable<OutputNode> {
|
||||
public Node outputLabel;
|
||||
public Address address;
|
||||
public long amount;
|
||||
|
||||
public OutputNode(Node outputLabel, Address address, long amount) {
|
||||
this.outputLabel = outputLabel;
|
||||
this.address = address;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(TransactionDiagram.OutputNode o) {
|
||||
try {
|
||||
return getOutputIndex(address, amount) - getOutputIndex(o.address, o.amount);
|
||||
} catch(Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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 SeedTool(), 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,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,12 +56,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 +71,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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 batchPageSize = 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 getBatchPageSize() {
|
||||
return batchPageSize;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -391,7 +414,7 @@ public class Hwi {
|
||||
}
|
||||
|
||||
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()));
|
||||
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);
|
||||
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
return elements;
|
||||
@@ -414,6 +437,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 +612,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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
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 SeedTool implements KeystoreFileImport {
|
||||
@Override
|
||||
public boolean isEncrypted(File file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Seed Tool";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.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 > Account Descriptor. Click the share icon at the bottom to show the QR.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFileFormatAvailable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -62,11 +62,11 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
wallet.setName(specterWallet.label);
|
||||
|
||||
if(specterWallet.devices != null && specterWallet.devices.size() == wallet.getKeystores().size()) {
|
||||
boolean uniqueLabels = specterWallet.devices.stream().map(d -> d.label).distinct().count() == specterWallet.devices.size();
|
||||
boolean uniqueLabels = specterWallet.devices.stream().map(SpecterWalletDevice::getLabel).distinct().count() == specterWallet.devices.size();
|
||||
for(int i = 0; i < specterWallet.devices.size(); i++) {
|
||||
SpecterWalletDevice device = specterWallet.devices.get(i);
|
||||
Keystore keystore = wallet.getKeystores().get(i);
|
||||
keystore.setLabel(device.label + (uniqueLabels ? "" : " " + i));
|
||||
keystore.setLabel(device.getLabel() + (uniqueLabels ? "" : " " + i));
|
||||
|
||||
WalletModel walletModel = device.getWalletModel();
|
||||
if(walletModel != null) {
|
||||
@@ -161,5 +161,13 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
if(label == null) {
|
||||
label = "Keystore";
|
||||
}
|
||||
|
||||
return label.length() > Keystore.MAX_LABEL_LENGTH - 3 ? label.substring(0, Keystore.MAX_LABEL_LENGTH - 3) : label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 == '{') {
|
||||
@@ -531,10 +554,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 +573,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());
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ 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);
|
||||
@@ -195,6 +196,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 +241,18 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
if(dirtyPersistables.label != null) {
|
||||
walletDao.updateLabel(wallet.getId(), dirtyPersistables.label.length() > 255 ? dirtyPersistables.label.substring(0, 255) : dirtyPersistables.label);
|
||||
}
|
||||
|
||||
if(dirtyPersistables.blockHeight != null) {
|
||||
walletDao.updateStoredBlockHeight(wallet.getId(), dirtyPersistables.blockHeight);
|
||||
}
|
||||
|
||||
if(dirtyPersistables.gapLimit != null) {
|
||||
walletDao.updateGapLimit(wallet.getId(), dirtyPersistables.gapLimit);
|
||||
}
|
||||
|
||||
if(!dirtyPersistables.labelEntries.isEmpty()) {
|
||||
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class);
|
||||
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class);
|
||||
@@ -622,6 +636,13 @@ public class DbPersistence implements Persistence {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletDeleted(WalletDeletedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).deleteAccount = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryCleared(WalletHistoryClearedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
@@ -636,6 +657,13 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletLabelChanged(WalletLabelChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).label = event.getLabel();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletBlockHeightChanged(WalletBlockHeightChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
@@ -643,6 +671,13 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletGapLimitChanged(WalletGapLimitChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).gapLimit = event.getGapLimit();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletEntryLabelsChanged(WalletEntryLabelsChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
@@ -687,9 +722,12 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
|
||||
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 final List<Entry> labelEntries = new ArrayList<>();
|
||||
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>();
|
||||
public boolean mixConfig;
|
||||
@@ -700,9 +738,12 @@ 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 +
|
||||
"\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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public class MixConfigMapper implements RowMapper<MixConfig> {
|
||||
mixOnStartup = null;
|
||||
}
|
||||
|
||||
String indexRange = rs.getString("indexRange");
|
||||
String mixToWalletFile = rs.getString("mixToWalletFile");
|
||||
String mixToWalletName = rs.getString("mixToWalletName");
|
||||
|
||||
@@ -26,7 +27,7 @@ public class MixConfigMapper implements RowMapper<MixConfig> {
|
||||
minMixes = null;
|
||||
}
|
||||
|
||||
MixConfig mixConfig = new MixConfig(scode, mixOnStartup, mixToWalletFile == null ? null : new File(mixToWalletFile), mixToWalletName, minMixes, rs.getInt("receiveIndex"), rs.getInt("changeIndex"));
|
||||
MixConfig mixConfig = new MixConfig(scode, mixOnStartup, indexRange, mixToWalletFile == null ? null : new File(mixToWalletFile), mixToWalletName, minMixes, rs.getInt("receiveIndex"), rs.getInt("changeIndex"));
|
||||
mixConfig.setId(rs.getLong("id"));
|
||||
return mixConfig;
|
||||
}
|
||||
|
||||
@@ -36,25 +36,31 @@ public interface WalletDao {
|
||||
@CreateSqlObject
|
||||
UtxoMixDataDao createUtxoMixDataDao();
|
||||
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id")
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.label, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id")
|
||||
@RegisterRowMapper(WalletMapper.class)
|
||||
List<Wallet> loadAllWallets();
|
||||
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id = 1")
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.label, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id = 1")
|
||||
@RegisterRowMapper(WalletMapper.class)
|
||||
Wallet loadMainWallet();
|
||||
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id != 1")
|
||||
@SqlQuery("select wallet.id, wallet.name, wallet.label, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id != 1")
|
||||
@RegisterRowMapper(WalletMapper.class)
|
||||
List<Wallet> loadChildWallets();
|
||||
|
||||
@SqlUpdate("insert into wallet (name, network, policyType, scriptType, storedBlockHeight, gapLimit, birthDate, defaultPolicy) values (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@SqlUpdate("insert into wallet (name, label, network, policyType, scriptType, storedBlockHeight, gapLimit, birthDate, defaultPolicy) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insert(String name, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Date birthDate, long defaultPolicy);
|
||||
long insert(String name, String label, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Date birthDate, long defaultPolicy);
|
||||
|
||||
@SqlUpdate("update wallet set label = :label where id = :id")
|
||||
void updateLabel(@Bind("id") long id, @Bind("label") String label);
|
||||
|
||||
@SqlUpdate("update wallet set storedBlockHeight = :blockHeight where id = :id")
|
||||
void updateStoredBlockHeight(@Bind("id") long id, @Bind("blockHeight") Integer blockHeight);
|
||||
|
||||
@SqlUpdate("update wallet set gapLimit = :gapLimit where id = :id")
|
||||
void updateGapLimit(@Bind("id") long id, @Bind("gapLimit") Integer gapLimit);
|
||||
|
||||
@SqlUpdate("set schema ?")
|
||||
int setSchema(String schema);
|
||||
|
||||
@@ -105,7 +111,7 @@ public interface WalletDao {
|
||||
setSchema(schema);
|
||||
createPolicyDao().addPolicy(wallet.getDefaultPolicy());
|
||||
|
||||
long id = insert(truncate(wallet.getName()), wallet.getNetwork().ordinal(), wallet.getPolicyType().ordinal(), wallet.getScriptType().ordinal(), wallet.getStoredBlockHeight(), wallet.gapLimit(), wallet.getBirthDate(), wallet.getDefaultPolicy().getId());
|
||||
long id = insert(truncate(wallet.getName()), truncate(wallet.getLabel()), wallet.getNetwork().ordinal(), wallet.getPolicyType().ordinal(), wallet.getScriptType().ordinal(), wallet.getStoredBlockHeight(), wallet.gapLimit(), wallet.getBirthDate(), wallet.getDefaultPolicy().getId());
|
||||
wallet.setId(id);
|
||||
|
||||
createKeystoreDao().addKeystores(wallet);
|
||||
|
||||
@@ -17,6 +17,7 @@ public class WalletMapper implements RowMapper<Wallet> {
|
||||
public Wallet map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
Wallet wallet = new Wallet(rs.getString("wallet.name"));
|
||||
wallet.setId(rs.getLong("wallet.id"));
|
||||
wallet.setLabel(rs.getString("wallet.label"));
|
||||
wallet.setNetwork(Network.values()[rs.getInt("wallet.network")]);
|
||||
wallet.setPolicyType(PolicyType.values()[rs.getInt("wallet.policyType")]);
|
||||
wallet.setScriptType(ScriptType.values()[rs.getInt("wallet.scriptType")]);
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
import org.jdbi.v3.sqlobject.statement.UseRowReducer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@@ -60,7 +61,8 @@ public interface WalletNodeDao {
|
||||
for(WalletNode purposeNode : wallet.getPurposeNodes()) {
|
||||
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), truncate(purposeNode.getLabel()), wallet.getId(), null);
|
||||
purposeNode.setId(purposeNodeId);
|
||||
for(WalletNode addressNode : purposeNode.getChildren()) {
|
||||
List<WalletNode> childNodes = new ArrayList<>(purposeNode.getChildren());
|
||||
for(WalletNode addressNode : childNodes) {
|
||||
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), truncate(addressNode.getLabel()), wallet.getId(), purposeNodeId);
|
||||
addressNode.setId(addressNodeId);
|
||||
addTransactionOutputs(addressNode);
|
||||
|
||||
@@ -18,14 +18,16 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
public void initializeView() {
|
||||
List<KeystoreFileImport> importers = Collections.emptyList();
|
||||
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
|
||||
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new SpecterDIY());
|
||||
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new SeedTool(), new SpecterDIY());
|
||||
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
|
||||
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new SpecterDIY());
|
||||
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new SeedTool(), new SpecterDIY());
|
||||
}
|
||||
|
||||
for(KeystoreImport importer : importers) {
|
||||
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer);;
|
||||
importAccordion.getPanes().add(importPane);
|
||||
for(KeystoreFileImport importer : importers) {
|
||||
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
|
||||
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == importer.getWalletModel()) {
|
||||
importAccordion.getPanes().add(importPane);
|
||||
}
|
||||
}
|
||||
|
||||
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||
|
||||
@@ -13,8 +13,10 @@ public class HwUsbDevicesController extends KeystoreImportDetailController {
|
||||
|
||||
public void initializeView(List<Device> devices) {
|
||||
for(Device device : devices) {
|
||||
DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1);
|
||||
deviceAccordion.getPanes().add(devicePane);
|
||||
DevicePane devicePane = new DevicePane(getMasterController().getWallet(), device, devices.size() == 1, getMasterController().getRequiredDerivation());
|
||||
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == device.getModel()) {
|
||||
deviceAccordion.getPanes().add(devicePane);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+25
-2
@@ -1,7 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.keystoreimport;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import javafx.application.Platform;
|
||||
@@ -10,6 +12,7 @@ import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Toggle;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
@@ -27,6 +30,9 @@ public class KeystoreImportController implements Initializable {
|
||||
@FXML
|
||||
private StackPane importPane;
|
||||
|
||||
private KeyDerivation requiredDerivation;
|
||||
private WalletModel requiredModel;
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
|
||||
@@ -53,11 +59,12 @@ public class KeystoreImportController implements Initializable {
|
||||
});
|
||||
}
|
||||
|
||||
public void selectSource(KeystoreSource keystoreSource) {
|
||||
public void selectSource(KeystoreSource keystoreSource, boolean required) {
|
||||
for(Toggle toggle : importMenu.getToggles()) {
|
||||
if(toggle.getUserData().equals(keystoreSource)) {
|
||||
Platform.runLater(() -> importMenu.selectToggle(toggle));
|
||||
return;
|
||||
} else if(required) {
|
||||
((ToggleButton)toggle).setDisable(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,4 +103,20 @@ public class KeystoreImportController implements Initializable {
|
||||
throw new IllegalStateException("Can't find pane", e);
|
||||
}
|
||||
}
|
||||
|
||||
public KeyDerivation getRequiredDerivation() {
|
||||
return requiredDerivation;
|
||||
}
|
||||
|
||||
public void setRequiredDerivation(KeyDerivation requiredDerivation) {
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
}
|
||||
|
||||
public WalletModel getRequiredModel() {
|
||||
return requiredModel;
|
||||
}
|
||||
|
||||
public void setRequiredModel(WalletModel requiredModel) {
|
||||
this.requiredModel = requiredModel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,10 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
|
||||
}
|
||||
|
||||
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource) {
|
||||
this(wallet, initialSource, null, null, false);
|
||||
}
|
||||
|
||||
public KeystoreImportDialog(Wallet wallet, KeystoreSource initialSource, KeyDerivation requiredDerivation, WalletModel requiredModel, boolean restrictSource) {
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
@@ -39,11 +43,16 @@ public class KeystoreImportDialog extends Dialog<Keystore> {
|
||||
dialogPane.setContent(Borders.wrap(ksiLoader.load()).emptyBorder().buildAll());
|
||||
keystoreImportController = ksiLoader.getController();
|
||||
keystoreImportController.initializeView(wallet);
|
||||
keystoreImportController.selectSource(initialSource);
|
||||
keystoreImportController.selectSource(initialSource, restrictSource);
|
||||
keystoreImportController.setRequiredDerivation(requiredDerivation);
|
||||
keystoreImportController.setRequiredModel(requiredModel);
|
||||
|
||||
final ButtonType watchOnlyButtonType = new javafx.scene.control.ButtonType(Network.get().getXpubHeader().getDisplayName() + " / Watch Only Wallet", ButtonBar.ButtonData.LEFT);
|
||||
if(!restrictSource) {
|
||||
dialogPane.getButtonTypes().add(watchOnlyButtonType);
|
||||
}
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(watchOnlyButtonType, cancelButtonType);
|
||||
dialogPane.getButtonTypes().add(cancelButtonType);
|
||||
dialogPane.setPrefWidth(650);
|
||||
dialogPane.setPrefHeight(690);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
@@ -21,7 +21,7 @@ public class SwController extends KeystoreImportDetailController {
|
||||
TitledDescriptionPane importPane = null;
|
||||
|
||||
if(importer instanceof KeystoreFileImport) {
|
||||
importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer);
|
||||
importPane = new FileKeystoreImportPane(getMasterController().getWallet(), (KeystoreFileImport)importer, getMasterController().getRequiredDerivation());
|
||||
} else if(importer instanceof KeystoreMnemonicImport) {
|
||||
importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer);
|
||||
} else if(importer instanceof KeystoreXprvImport) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
public class AllHistoryChangedException extends RuntimeException {
|
||||
public AllHistoryChangedException() {
|
||||
}
|
||||
|
||||
public AllHistoryChangedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AllHistoryChangedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public AllHistoryChangedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import com.github.arteam.simplejsonrpc.client.Transport;
|
||||
import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder;
|
||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException;
|
||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -19,10 +17,12 @@ import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.drongo.wallet.WalletNode.nodeRangesToString;
|
||||
|
||||
public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
private static final Logger log = LoggerFactory.getLogger(BatchedElectrumServerRpc.class);
|
||||
private static final int MAX_RETRIES = 5;
|
||||
private static final int RETRY_DELAY = 1;
|
||||
static final int MAX_RETRIES = 5;
|
||||
static final int RETRY_DELAY = 1;
|
||||
|
||||
private final AtomicLong idCounter = new AtomicLong();
|
||||
|
||||
@@ -73,19 +73,18 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, ScriptHashTx[]> getScriptHashHistory(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes, boolean failOnError) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class);
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + getScriptHashesAbbreviation(pathScriptHashes.keySet())));
|
||||
PagedBatchRequestBuilder<String, ScriptHashTx[]> batchRequest = PagedBatchRequestBuilder.create(transport).keysType(String.class).returnType(ScriptHashTx[].class);
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + nodeRangesToString(pathScriptHashes.keySet())));
|
||||
|
||||
for(String path : pathScriptHashes.keySet()) {
|
||||
batchRequest.add(path, "blockchain.scripthash.get_history", pathScriptHashes.get(path));
|
||||
}
|
||||
|
||||
try {
|
||||
return new RetryLogic<Map<String, ScriptHashTx[]>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
|
||||
return batchRequest.execute();
|
||||
} catch (JsonRpcBatchException e) {
|
||||
if(failOnError) {
|
||||
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + getScriptHashesAbbreviation((Collection<String>)e.getErrors().keySet()), e);
|
||||
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + nodeRangesToString((Collection<String>)e.getErrors().keySet()), e);
|
||||
}
|
||||
|
||||
Map<String, ScriptHashTx[]> result = (Map<String, ScriptHashTx[]>)e.getSuccesses();
|
||||
@@ -95,25 +94,24 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
|
||||
return result;
|
||||
} catch(Exception e) {
|
||||
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + getScriptHashesAbbreviation(pathScriptHashes.keySet()), e);
|
||||
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + nodeRangesToString(pathScriptHashes.keySet()), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, ScriptHashTx[]> getScriptHashMempool(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes, boolean failOnError) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class);
|
||||
PagedBatchRequestBuilder<String, ScriptHashTx[]> batchRequest = PagedBatchRequestBuilder.create(transport).keysType(String.class).returnType(ScriptHashTx[].class);
|
||||
|
||||
for(String path : pathScriptHashes.keySet()) {
|
||||
batchRequest.add(path, "blockchain.scripthash.get_mempool", pathScriptHashes.get(path));
|
||||
}
|
||||
|
||||
try {
|
||||
return new RetryLogic<Map<String, ScriptHashTx[]>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
|
||||
return batchRequest.execute();
|
||||
} catch(JsonRpcBatchException e) {
|
||||
if(failOnError) {
|
||||
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + getScriptHashesAbbreviation((Collection<String>)e.getErrors().keySet()), e);
|
||||
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + nodeRangesToString((Collection<String>)e.getErrors().keySet()), e);
|
||||
}
|
||||
|
||||
Map<String, ScriptHashTx[]> result = (Map<String, ScriptHashTx[]>)e.getSuccesses();
|
||||
@@ -123,36 +121,34 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
|
||||
return result;
|
||||
} catch(Exception e) {
|
||||
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + getScriptHashesAbbreviation(pathScriptHashes.keySet()), e);
|
||||
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + nodeRangesToString(pathScriptHashes.keySet()), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, String> subscribeScriptHashes(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + getScriptHashesAbbreviation(pathScriptHashes.keySet())));
|
||||
PagedBatchRequestBuilder<String, String> batchRequest = PagedBatchRequestBuilder.create(transport).keysType(String.class).returnType(String.class);
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + nodeRangesToString(pathScriptHashes.keySet())));
|
||||
|
||||
for(String path : pathScriptHashes.keySet()) {
|
||||
batchRequest.add(path, "blockchain.scripthash.subscribe", pathScriptHashes.get(path));
|
||||
}
|
||||
|
||||
try {
|
||||
return new RetryLogic<Map<String, String>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
|
||||
return batchRequest.execute();
|
||||
} catch(JsonRpcBatchException e) {
|
||||
//Even if we have some successes, failure to subscribe for all script hashes will result in outdated wallet view. Don't proceed.
|
||||
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + getScriptHashesAbbreviation((Collection<String>)e.getErrors().keySet()), e);
|
||||
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + nodeRangesToString((Collection<String>)e.getErrors().keySet()), e);
|
||||
} catch(Exception e) {
|
||||
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + getScriptHashesAbbreviation(pathScriptHashes.keySet()), e);
|
||||
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + nodeRangesToString(pathScriptHashes.keySet()), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BatchRequestBuilder<Integer, String> batchRequest = client.createBatchRequest().keysType(Integer.class).returnType(String.class);
|
||||
PagedBatchRequestBuilder<Integer, String> batchRequest = PagedBatchRequestBuilder.create(transport).keysType(Integer.class).returnType(String.class);
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving " + blockHeights.size() + " block headers"));
|
||||
|
||||
for(Integer height : blockHeights) {
|
||||
@@ -160,7 +156,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
}
|
||||
|
||||
try {
|
||||
return new RetryLogic<Map<Integer, String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute);
|
||||
return batchRequest.execute();
|
||||
} catch(JsonRpcBatchException e) {
|
||||
return (Map<Integer, String>)e.getSuccesses();
|
||||
} catch(Exception e) {
|
||||
@@ -171,8 +167,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
|
||||
PagedBatchRequestBuilder<String, String> batchRequest = PagedBatchRequestBuilder.create(transport).keysType(String.class).returnType(String.class);
|
||||
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving " + txids.size() + " transactions"));
|
||||
|
||||
for(String txid : txids) {
|
||||
@@ -180,7 +175,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
}
|
||||
|
||||
try {
|
||||
return new RetryLogic<Map<String, String>>(MAX_RETRIES, RETRY_DELAY, IllegalStateException.class).getResult(batchRequest::execute);
|
||||
return batchRequest.execute();
|
||||
} catch(JsonRpcBatchException e) {
|
||||
Map<String, String> result = (Map<String, String>)e.getSuccesses();
|
||||
|
||||
@@ -276,65 +271,4 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
|
||||
throw new ElectrumServerRpcException("Error broadcasting transaction", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getScriptHashesAbbreviation(Collection<String> scriptHashes) {
|
||||
List<String> sortedHashes = new ArrayList<>(scriptHashes);
|
||||
|
||||
if(scriptHashes.isEmpty()) {
|
||||
return "[]";
|
||||
}
|
||||
|
||||
List<List<String>> contiguous = splitToContiguous(sortedHashes);
|
||||
|
||||
String abbrev = "[";
|
||||
for(Iterator<List<String>> iter = contiguous.iterator(); iter.hasNext(); ) {
|
||||
List<String> range = iter.next();
|
||||
abbrev += range.get(0);
|
||||
if(range.size() > 1) {
|
||||
abbrev += "-" + range.get(range.size() - 1);
|
||||
}
|
||||
if(iter.hasNext()) {
|
||||
abbrev += ", ";
|
||||
}
|
||||
}
|
||||
abbrev += "]";
|
||||
|
||||
return abbrev;
|
||||
}
|
||||
|
||||
static List<List<String>> splitToContiguous(List<String> input) {
|
||||
List<List<String>> result = new ArrayList<>();
|
||||
int prev = 0;
|
||||
|
||||
int keyPurpose = getKeyPurpose(input.get(0));
|
||||
int index = getIndex(input.get(0));
|
||||
|
||||
for (int cur = 0; cur < input.size(); cur++) {
|
||||
if(getKeyPurpose(input.get(cur)) != keyPurpose || getIndex(input.get(cur)) != index) {
|
||||
result.add(input.subList(prev, cur));
|
||||
prev = cur;
|
||||
}
|
||||
index = getIndex(input.get(cur)) + 1;
|
||||
keyPurpose = getKeyPurpose(input.get(cur));
|
||||
}
|
||||
result.add(input.subList(prev, input.size()));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int getKeyPurpose(String path) {
|
||||
List<ChildNumber> childNumbers = KeyDerivation.parsePath(path);
|
||||
if(childNumbers.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
return childNumbers.get(0).num();
|
||||
}
|
||||
|
||||
private static int getIndex(String path) {
|
||||
List<ChildNumber> childNumbers = KeyDerivation.parsePath(path);
|
||||
if(childNumbers.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
return childNumbers.get(childNumbers.size() - 1).num();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
@@ -7,6 +8,7 @@ import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
@@ -100,6 +102,7 @@ public class Bwt {
|
||||
bwtConfig.gapLimit = gapLimit;
|
||||
} else {
|
||||
bwtConfig.requireAddresses = false;
|
||||
bwtConfig.bitcoindTimeout = 30;
|
||||
}
|
||||
|
||||
bwtConfig.verbose = log.isDebugEnabled() ? 2 : 0;
|
||||
@@ -112,15 +115,20 @@ public class Bwt {
|
||||
|
||||
Config config = Config.get();
|
||||
bwtConfig.bitcoindUrl = config.getCoreServer();
|
||||
if(config.getCoreAuthType() == CoreAuthType.COOKIE && config.getCoreDataDir() != null) {
|
||||
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(Protocol.isOnionAddress(bwtConfig.bitcoindUrl) && torProxy != null) {
|
||||
bwtConfig.bitcoindProxy = torProxy.toString();
|
||||
}
|
||||
|
||||
if((config.getCoreAuthType() == CoreAuthType.COOKIE || config.getCoreAuth() == null || config.getCoreAuth().length() < 2) && config.getCoreDataDir() != null) {
|
||||
bwtConfig.bitcoindDir = config.getCoreDataDir().getAbsolutePath() + "/";
|
||||
} else {
|
||||
bwtConfig.bitcoindAuth = config.getCoreAuth();
|
||||
}
|
||||
if(config.getCoreMultiWallet() != Boolean.FALSE) {
|
||||
bwtConfig.bitcoindWallet = config.getCoreWallet();
|
||||
}
|
||||
bwtConfig.createWalletIfMissing = !outputDescriptors.isEmpty();
|
||||
|
||||
bwtConfig.bitcoindWallet = DEFAULT_CORE_WALLET;
|
||||
bwtConfig.createWalletIfMissing = true;
|
||||
|
||||
Gson gson = new Gson();
|
||||
String jsonConfig = gson.toJson(bwtConfig);
|
||||
@@ -129,6 +137,12 @@ public class Bwt {
|
||||
NativeBwtDaemon.start(jsonConfig, callback);
|
||||
}
|
||||
|
||||
private HostAndPort getTorProxy() {
|
||||
return AppServices.isTorRunning() ?
|
||||
HostAndPort.fromParts("127.0.0.1", TorService.PROXY_PORT) :
|
||||
(Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer().replace("localhost", "127.0.0.1")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down the BWT daemon
|
||||
*
|
||||
@@ -184,6 +198,12 @@ public class Bwt {
|
||||
@SerializedName("bitcoind_wallet")
|
||||
public String bitcoindWallet;
|
||||
|
||||
@SerializedName("bitcoind_proxy")
|
||||
public String bitcoindProxy;
|
||||
|
||||
@SerializedName("bitcoind_timeout")
|
||||
public Integer bitcoindTimeout;
|
||||
|
||||
@SerializedName("create_wallet_if_missing")
|
||||
public Boolean createWalletIfMissing;
|
||||
|
||||
|
||||
@@ -167,8 +167,14 @@ public class ElectrumServer {
|
||||
}
|
||||
|
||||
public static void addCalculatedScriptHashes(Wallet wallet) {
|
||||
calculateScriptHashes(wallet, KeyPurpose.RECEIVE).forEach(retrievedScriptHashes::putIfAbsent);
|
||||
calculateScriptHashes(wallet, KeyPurpose.CHANGE).forEach(retrievedScriptHashes::putIfAbsent);
|
||||
getCalculatedScriptHashes(wallet).forEach(retrievedScriptHashes::putIfAbsent);
|
||||
}
|
||||
|
||||
private static Map<String, String> getCalculatedScriptHashes(Wallet wallet) {
|
||||
Map<String, String> storedScriptHashStatuses = new HashMap<>();
|
||||
storedScriptHashStatuses.putAll(calculateScriptHashes(wallet, KeyPurpose.RECEIVE));
|
||||
storedScriptHashStatuses.putAll(calculateScriptHashes(wallet, KeyPurpose.CHANGE));
|
||||
return storedScriptHashStatuses;
|
||||
}
|
||||
|
||||
private static Map<String, String> calculateScriptHashes(Wallet wallet, KeyPurpose keyPurpose) {
|
||||
@@ -705,7 +711,7 @@ public class ElectrumServer {
|
||||
}
|
||||
|
||||
public void copyPostmixLabels(Wallet wallet, Set<BlockTransactionHashIndex> newTransactionOutputs) {
|
||||
if(wallet.getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX) {
|
||||
if(wallet.getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && wallet.getMasterWallet() != null) {
|
||||
for(BlockTransactionHashIndex newRef : newTransactionOutputs) {
|
||||
BlockTransactionHashIndex prevRef = wallet.getWalletTxos().keySet().stream()
|
||||
.filter(txo -> wallet.getMasterWallet().getUtxoMixData(txo) != null && txo.isSpent() && txo.getSpentBy().getHash().equals(newRef.getHash())).findFirst().orElse(null);
|
||||
@@ -891,6 +897,10 @@ public class ElectrumServer {
|
||||
|
||||
if(server.startsWith("electrs/")) {
|
||||
String electrsVersion = server.substring("electrs/".length());
|
||||
int dashIndex = electrsVersion.indexOf('-');
|
||||
if(dashIndex > -1) {
|
||||
electrsVersion = electrsVersion.substring(0, dashIndex);
|
||||
}
|
||||
try {
|
||||
Version version = new Version(electrsVersion);
|
||||
if(version.compareTo(ELECTRS_MIN_BATCHING_VERSION) >= 0) {
|
||||
@@ -1184,14 +1194,30 @@ public class ElectrumServer {
|
||||
synchronized(walletSynchronizeLocks.get(wallet)) {
|
||||
if(isConnected()) {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
Map<String, String> previousScriptHashes = getCalculatedScriptHashes(wallet);
|
||||
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = (nodes == null ? electrumServer.getHistory(wallet) : electrumServer.getHistory(wallet, nodes));
|
||||
electrumServer.getReferencedTransactions(wallet, nodeTransactionMap);
|
||||
electrumServer.calculateNodeHistory(wallet, nodeTransactionMap);
|
||||
|
||||
//Add all of the script hashes we have now fetched the history for so we don't need to fetch again until the script hash status changes
|
||||
for(WalletNode node : (nodes == null ? nodeTransactionMap.keySet() : nodes)) {
|
||||
Set<WalletNode> updatedNodes = new HashSet<>();
|
||||
Map<WalletNode, Set<BlockTransactionHashIndex>> walletNodes = wallet.getWalletNodes();
|
||||
for(WalletNode node : (nodes == null ? walletNodes.keySet() : nodes)) {
|
||||
String scriptHash = getScriptHash(wallet, node);
|
||||
retrievedScriptHashes.put(scriptHash, getSubscribedScriptHashStatus(scriptHash));
|
||||
String subscribedStatus = getSubscribedScriptHashStatus(scriptHash);
|
||||
if(!Objects.equals(subscribedStatus, retrievedScriptHashes.get(scriptHash))) {
|
||||
updatedNodes.add(node);
|
||||
}
|
||||
retrievedScriptHashes.put(scriptHash, subscribedStatus);
|
||||
}
|
||||
|
||||
//If wallet was not empty, check if all used updated nodes have changed history
|
||||
if(nodes == null && previousScriptHashes.values().stream().anyMatch(Objects::nonNull)) {
|
||||
if(!updatedNodes.isEmpty() && updatedNodes.equals(walletNodes.entrySet().stream().filter(entry -> !entry.getValue().isEmpty()).map(Map.Entry::getKey).collect(Collectors.toSet()))) {
|
||||
//All used nodes on a non-empty wallet have changed history. Abort and trigger a full refresh.
|
||||
log.info("All used nodes on a non-empty wallet have changed history. Triggering a full wallet refresh.");
|
||||
throw new AllHistoryChangedException();
|
||||
}
|
||||
}
|
||||
|
||||
//Clear transaction outputs for nodes that have no history - this is useful when a transaction is replaced in the mempool
|
||||
@@ -1411,4 +1437,48 @@ public class ElectrumServer {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class WalletDiscoveryService extends Service<List<StandardAccount>> {
|
||||
private final Wallet masterWalletCopy;
|
||||
private final List<StandardAccount> standardAccounts;
|
||||
private final Map<StandardAccount, Keystore> importedKeystores;
|
||||
|
||||
public WalletDiscoveryService(Wallet masterWallet, List<StandardAccount> standardAccounts) {
|
||||
this.masterWalletCopy = masterWallet.copy();
|
||||
this.standardAccounts = standardAccounts;
|
||||
this.importedKeystores = new HashMap<>();
|
||||
}
|
||||
|
||||
public WalletDiscoveryService(Wallet masterWallet, Map<StandardAccount, Keystore> importedKeystores) {
|
||||
this.masterWalletCopy = masterWallet.copy();
|
||||
this.standardAccounts = new ArrayList<>(importedKeystores.keySet());
|
||||
this.importedKeystores = importedKeystores;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<List<StandardAccount>> createTask() {
|
||||
return new Task<>() {
|
||||
protected List<StandardAccount> call() throws ServerException {
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
List<StandardAccount> discoveredAccounts = new ArrayList<>();
|
||||
|
||||
for(StandardAccount standardAccount : standardAccounts) {
|
||||
Wallet wallet = masterWalletCopy.addChildWallet(standardAccount);
|
||||
if(importedKeystores.containsKey(standardAccount)) {
|
||||
wallet.getKeystores().clear();
|
||||
wallet.getKeystores().add(importedKeystores.get(standardAccount));
|
||||
}
|
||||
|
||||
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>();
|
||||
electrumServer.getReferences(wallet, wallet.getNode(KeyPurpose.RECEIVE).getChildren(), nodeTransactionMap, 0);
|
||||
if(nodeTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
|
||||
discoveredAccounts.add(standardAccount);
|
||||
}
|
||||
}
|
||||
|
||||
return discoveredAccounts;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,17 @@ public enum FeeRatesSource {
|
||||
String url = "https://bitcoinfees.earn.com/api/v1/fees/recommended";
|
||||
return getThreeTierFeeRates(defaultblockTargetFeeRates, url);
|
||||
}
|
||||
},
|
||||
MINIMUM("Minimum (1 sat/vB)") {
|
||||
@Override
|
||||
public Map<Integer, Double> getBlockTargetFeeRates(Map<Integer, Double> defaultblockTargetFeeRates) {
|
||||
Map<Integer, Double> blockTargetFeeRates = new LinkedHashMap<>();
|
||||
for(Integer blockTarget : defaultblockTargetFeeRates.keySet()) {
|
||||
blockTargetFeeRates.put(blockTarget, 1.0);
|
||||
}
|
||||
|
||||
return blockTargetFeeRates;
|
||||
}
|
||||
};
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(FeeRatesSource.class);
|
||||
|
||||
@@ -102,6 +102,6 @@ public final class IpAddressMatcher {
|
||||
}
|
||||
|
||||
public static boolean isLocalNetworkAddress(String address) {
|
||||
return LOCAL_RANGE_1.matches(address) || LOCAL_RANGE_2.matches(address) || LOCAL_RANGE_3.matches(address);
|
||||
return "localhost".equals(address) || "127.0.0.1".equals(address) || LOCAL_RANGE_1.matches(address) || LOCAL_RANGE_2.matches(address) || LOCAL_RANGE_3.matches(address);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.arteam.simplejsonrpc.client.JsonRpcClient;
|
||||
import com.github.arteam.simplejsonrpc.client.Transport;
|
||||
import com.github.arteam.simplejsonrpc.client.builder.AbstractBuilder;
|
||||
import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.sparrow.net.BatchedElectrumServerRpc.MAX_RETRIES;
|
||||
import static com.sparrowwallet.sparrow.net.BatchedElectrumServerRpc.RETRY_DELAY;
|
||||
|
||||
public class PagedBatchRequestBuilder<K, V> extends AbstractBuilder {
|
||||
public static final int DEFAULT_PAGE_SIZE = 500;
|
||||
|
||||
@NotNull
|
||||
private final List<Request> requests;
|
||||
|
||||
/**
|
||||
* Type of request ids
|
||||
*/
|
||||
@Nullable
|
||||
private final Class<K> keysType;
|
||||
|
||||
/**
|
||||
* Expected return type for all requests
|
||||
* <p/>
|
||||
* This property works exclusively with {@code returnTypes}. Only one of them should be set.
|
||||
*/
|
||||
@Nullable
|
||||
private final Class<V> returnType;
|
||||
|
||||
/**
|
||||
* Creates a new batch request builder in an initial state
|
||||
*
|
||||
* @param transport transport for request performing
|
||||
* @param mapper mapper for JSON processing
|
||||
*/
|
||||
public PagedBatchRequestBuilder(@NotNull Transport transport, @NotNull ObjectMapper mapper) {
|
||||
this(transport, mapper, new ArrayList<Request>(), null, null);
|
||||
}
|
||||
|
||||
public PagedBatchRequestBuilder(@NotNull Transport transport, @NotNull ObjectMapper mapper,
|
||||
@NotNull List<Request> requests,
|
||||
@Nullable Class<K> keysType, @Nullable Class<V> returnType) {
|
||||
super(transport, mapper);
|
||||
this.requests = requests;
|
||||
this.keysType = keysType;
|
||||
this.returnType = returnType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new request without specifying a return type
|
||||
*
|
||||
* @param id request id as a text value
|
||||
* @param method request method
|
||||
* @param param request param
|
||||
* @return the current builder
|
||||
*/
|
||||
@NotNull
|
||||
public PagedBatchRequestBuilder<K, V> add(Object id, @NotNull String method, @NotNull Object param) {
|
||||
requests.add(new Request(id, method, param));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets type of request keys.
|
||||
* The purpose of this method is providing static and runtime type safety of processing of batch responses
|
||||
*
|
||||
* @param keysClass type of keys
|
||||
* @param <NK> type of keys
|
||||
* @return a new builder
|
||||
*/
|
||||
public <NK> PagedBatchRequestBuilder<NK, V> keysType(@NotNull Class<NK> keysClass) {
|
||||
return new PagedBatchRequestBuilder<NK, V>(transport, mapper, requests, keysClass, returnType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an expected response type of requests.
|
||||
* This method is preferred when requests have the same response type.
|
||||
*
|
||||
* @param valuesClass expected requests return type
|
||||
* @param <NV> expected requests return type
|
||||
* @return a new builder
|
||||
*/
|
||||
public <NV> PagedBatchRequestBuilder<K, NV> returnType(@NotNull Class<NV> valuesClass) {
|
||||
return new PagedBatchRequestBuilder<K, NV>(transport, mapper, requests, keysType, valuesClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates, executes the request and process response
|
||||
*
|
||||
* @return map of responses by request ids
|
||||
*/
|
||||
@NotNull
|
||||
public Map<K, V> execute() throws Exception {
|
||||
Map<K, V> allResults = new HashMap<>();
|
||||
JsonRpcClient client = new JsonRpcClient(transport);
|
||||
|
||||
List<List<Request>> pages = Lists.partition(requests, getPageSize());
|
||||
for(List<Request> page : pages) {
|
||||
BatchRequestBuilder<K, V> batchRequest = client.createBatchRequest().keysType(keysType).returnType(returnType);
|
||||
for(Request request : page) {
|
||||
if(request.id instanceof String strReq) {
|
||||
batchRequest.add(strReq, request.method, request.param);
|
||||
} else if(request.id instanceof Integer intReq) {
|
||||
batchRequest.add(intReq, request.method, request.param);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Id of class " + request.id.getClass().getName() + " not supported");
|
||||
}
|
||||
}
|
||||
|
||||
Map<K, V> pageResult = new RetryLogic<Map<K, V>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
|
||||
allResults.putAll(pageResult);
|
||||
}
|
||||
|
||||
return allResults;
|
||||
}
|
||||
|
||||
private int getPageSize() {
|
||||
int pageSize = Config.get().getBatchPageSize();
|
||||
if(pageSize < 1) {
|
||||
pageSize = DEFAULT_PAGE_SIZE;
|
||||
}
|
||||
|
||||
return pageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a builder of a JSON-RPC batch request in initial state
|
||||
*
|
||||
* @return batch request builder
|
||||
*/
|
||||
@NotNull
|
||||
public static PagedBatchRequestBuilder<?, ?> create(Transport transport) {
|
||||
return new PagedBatchRequestBuilder<Object, Object>(transport, new ObjectMapper());
|
||||
}
|
||||
|
||||
private static record Request(Object id, String method, Object param) {}
|
||||
}
|
||||
@@ -116,10 +116,21 @@ public enum Protocol {
|
||||
return toUrlString() + hostAndPort.toString();
|
||||
}
|
||||
|
||||
public boolean isOnionAddress(HostAndPort server) {
|
||||
public static boolean isOnionAddress(HostAndPort server) {
|
||||
return server.getHost().toLowerCase().endsWith(TorService.TOR_ADDRESS_SUFFIX);
|
||||
}
|
||||
|
||||
public static boolean isOnionAddress(String address) {
|
||||
if(address != null) {
|
||||
Protocol protocol = Protocol.getProtocol(address);
|
||||
if(protocol != null) {
|
||||
return isOnionAddress(protocol.getServerHostAndPort(address));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Protocol getProtocol(String url) {
|
||||
if(url.startsWith("tcp://")) {
|
||||
return TCP;
|
||||
@@ -133,4 +144,13 @@ public enum Protocol {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getHost(String url) {
|
||||
Protocol protocol = getProtocol(url);
|
||||
if(protocol != null) {
|
||||
return protocol.getServerHostAndPort(url).getHost();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
public enum PublicElectrumServer {
|
||||
BLOCKSTREAM_INFO("blockstream.info", "ssl://blockstream.info:700"),
|
||||
ELECTRUM_BLOCKSTREAM_INFO("electrum.blockstream.info", "ssl://electrum.blockstream.info:50002"),
|
||||
LUKECHILDS_CO("bitcoin.lukechilds.co", "ssl://bitcoin.lukechilds.co:50002"),
|
||||
EMZY_DE("electrum.emzy.de", "ssl://electrum.emzy.de:50002"),
|
||||
BITAROO_NET("electrum.bitaroo.net", "ssl://electrum.bitaroo.net:50002");
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
|
||||
PublicElectrumServer(String name, String url) {
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public enum PublicElectrumServer {
|
||||
BLOCKSTREAM_INFO("blockstream.info", "ssl://blockstream.info:700", Network.MAINNET),
|
||||
ELECTRUM_BLOCKSTREAM_INFO("electrum.blockstream.info", "ssl://electrum.blockstream.info:50002", Network.MAINNET),
|
||||
LUKECHILDS_CO("bitcoin.lukechilds.co", "ssl://bitcoin.lukechilds.co:50002", Network.MAINNET),
|
||||
EMZY_DE("electrum.emzy.de", "ssl://electrum.emzy.de:50002", Network.MAINNET),
|
||||
BITAROO_NET("electrum.bitaroo.net", "ssl://electrum.bitaroo.net:50002", Network.MAINNET),
|
||||
TESTNET_ARANGUREN_ORG("testnet.aranguren.org", "ssl://testnet.aranguren.org:51002", Network.TESTNET);
|
||||
|
||||
PublicElectrumServer(String name, String url, Network network) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.network = network;
|
||||
}
|
||||
|
||||
public static final List<Network> SUPPORTED_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
|
||||
private final String name;
|
||||
private final String url;
|
||||
private final Network network;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
@@ -23,6 +34,18 @@ public enum PublicElectrumServer {
|
||||
return url;
|
||||
}
|
||||
|
||||
public Network getNetwork() {
|
||||
return network;
|
||||
}
|
||||
|
||||
public static List<PublicElectrumServer> getServers() {
|
||||
return Arrays.stream(values()).filter(server -> server.network == Network.get()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public static boolean supportedNetwork() {
|
||||
return SUPPORTED_NETWORKS.contains(Network.get());
|
||||
}
|
||||
|
||||
public static PublicElectrumServer fromUrl(String url) {
|
||||
for(PublicElectrumServer server : values()) {
|
||||
if(server.url.equals(url)) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import java.io.*;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
@@ -25,12 +26,15 @@ public class TcpTransport implements Transport, Closeable {
|
||||
private static final Logger log = LoggerFactory.getLogger(TcpTransport.class);
|
||||
|
||||
public static final int DEFAULT_PORT = 50001;
|
||||
private static final int[] BASE_READ_TIMEOUT_SECS = {3, 8, 16, 34};
|
||||
public static final int DEFAULT_MAX_TIMEOUT = 34;
|
||||
private static final int[] BASE_READ_TIMEOUT_SECS = {3, 8, 16, DEFAULT_MAX_TIMEOUT};
|
||||
private static final int[] SLOW_READ_TIMEOUT_SECS = {34, 68, 124, 208};
|
||||
public static final long PER_REQUEST_READ_TIMEOUT_MILLIS = 50;
|
||||
public static final int SOCKET_READ_TIMEOUT_MILLIS = 5000;
|
||||
|
||||
protected final HostAndPort server;
|
||||
protected final SocketFactory socketFactory;
|
||||
protected final int[] readTimeouts;
|
||||
|
||||
private Socket socket;
|
||||
|
||||
@@ -59,6 +63,13 @@ public class TcpTransport implements Transport, Closeable {
|
||||
public TcpTransport(HostAndPort server, HostAndPort proxy) {
|
||||
this.server = server;
|
||||
this.socketFactory = (proxy == null ? SocketFactory.getDefault() : new ProxySocketFactory(proxy));
|
||||
|
||||
int[] timeouts = (Config.get().getServerType() == ServerType.BITCOIN_CORE && Protocol.isOnionAddress(Config.get().getCoreServer()) ?
|
||||
Arrays.copyOf(SLOW_READ_TIMEOUT_SECS, SLOW_READ_TIMEOUT_SECS.length) : Arrays.copyOf(BASE_READ_TIMEOUT_SECS, BASE_READ_TIMEOUT_SECS.length));
|
||||
if(Config.get().getMaxServerTimeout() > timeouts[timeouts.length - 1]) {
|
||||
timeouts[timeouts.length - 1] = Config.get().getMaxServerTimeout();
|
||||
}
|
||||
this.readTimeouts = timeouts;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,16 +102,16 @@ public class TcpTransport implements Transport, Closeable {
|
||||
|
||||
private String readResponse() throws IOException {
|
||||
try {
|
||||
if(!readLock.tryLock((BASE_READ_TIMEOUT_SECS[readTimeoutIndex] * 1000) + (requestIdCount * PER_REQUEST_READ_TIMEOUT_MILLIS), TimeUnit.MILLISECONDS)) {
|
||||
readTimeoutIndex = Math.min(readTimeoutIndex + 1, BASE_READ_TIMEOUT_SECS.length - 1);
|
||||
log.warn("No response from server, setting read timeout to " + BASE_READ_TIMEOUT_SECS[readTimeoutIndex] + " secs");
|
||||
if(!readLock.tryLock((readTimeouts[readTimeoutIndex] * 1000L) + (requestIdCount * PER_REQUEST_READ_TIMEOUT_MILLIS), TimeUnit.MILLISECONDS)) {
|
||||
readTimeoutIndex = Math.min(readTimeoutIndex + 1, readTimeouts.length - 1);
|
||||
log.warn("No response from server, setting read timeout to " + readTimeouts[readTimeoutIndex] + " secs");
|
||||
throw new IOException("No response from server");
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
throw new IOException("Read thread interrupted");
|
||||
}
|
||||
|
||||
if(readTimeoutIndex == BASE_READ_TIMEOUT_SECS.length - 1) {
|
||||
if(readTimeoutIndex == readTimeouts.length - 1) {
|
||||
readTimeoutIndex--;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,14 @@ package com.sparrowwallet.sparrow.net;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import net.freehaven.tor.control.TorControlError;
|
||||
import org.berndpruenster.netlayer.tor.NativeTor;
|
||||
import org.berndpruenster.netlayer.tor.Tor;
|
||||
import org.berndpruenster.netlayer.tor.TorCtlException;
|
||||
import org.berndpruenster.netlayer.tor.Torrc;
|
||||
import org.berndpruenster.netlayer.tor.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.Socket;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
@@ -40,6 +39,7 @@ public class TorService extends ScheduledService<NativeTor> {
|
||||
try {
|
||||
LinkedHashMap<String, String> torrcOptionsMap = new LinkedHashMap<>();
|
||||
torrcOptionsMap.put("SocksPort", Integer.toString(PROXY_PORT));
|
||||
torrcOptionsMap.put("HashedControlPassword", "16:D780432418F09B06609940000924317D3B9DF522A3191F8F4E597E9329");
|
||||
torrcOptionsMap.put("DisableNetwork", "0");
|
||||
Torrc override = new Torrc(torrcOptionsMap);
|
||||
|
||||
@@ -62,4 +62,25 @@ public class TorService extends ScheduledService<NativeTor> {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Socket getControlSocket() {
|
||||
Tor tor = Tor.getDefault();
|
||||
if(tor != null) {
|
||||
try {
|
||||
Class<?> torClass = Class.forName("org.berndpruenster.netlayer.tor.Tor");
|
||||
Field torControllerField = torClass.getDeclaredField("torController");
|
||||
torControllerField.setAccessible(true);
|
||||
TorController torController = (TorController)torControllerField.get(tor);
|
||||
|
||||
Class<?> torControllerClass = Class.forName("org.berndpruenster.netlayer.tor.TorController");
|
||||
Field socketField = torControllerClass.getDeclaredField("socket");
|
||||
socketField.setAccessible(true);
|
||||
return (Socket)socketField.get(torController);
|
||||
} catch(Exception e) {
|
||||
log.error("Error retrieving Tor control socket", e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+63
-30
@@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.Mode;
|
||||
import com.sparrowwallet.sparrow.control.ComboBoxTextField;
|
||||
import com.sparrowwallet.sparrow.control.TextFieldValidator;
|
||||
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
@@ -15,6 +16,7 @@ import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.text.Font;
|
||||
@@ -22,6 +24,7 @@ import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.berndpruenster.netlayer.tor.Tor;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
@@ -40,6 +43,7 @@ import java.io.FileInputStream;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
@@ -75,7 +79,10 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
private Form coreForm;
|
||||
|
||||
@FXML
|
||||
private TextField coreHost;
|
||||
private ComboBox<String> recentCoreServers;
|
||||
|
||||
@FXML
|
||||
private ComboBoxTextField coreHost;
|
||||
|
||||
@FXML
|
||||
private TextField corePort;
|
||||
@@ -102,16 +109,22 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
private PasswordField corePass;
|
||||
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch coreMultiWallet;
|
||||
private UnlabeledToggleSwitch coreUseProxy;
|
||||
|
||||
@FXML
|
||||
private TextField coreWallet;
|
||||
private TextField coreProxyHost;
|
||||
|
||||
@FXML
|
||||
private TextField coreProxyPort;
|
||||
|
||||
@FXML
|
||||
private Form electrumForm;
|
||||
|
||||
@FXML
|
||||
private TextField electrumHost;
|
||||
private ComboBox<String> recentElectrumServers;
|
||||
|
||||
@FXML
|
||||
private ComboBoxTextField electrumHost;
|
||||
|
||||
@FXML
|
||||
private TextField electrumPort;
|
||||
@@ -183,15 +196,16 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
}
|
||||
});
|
||||
ServerType serverType = config.getServerType() != null ?
|
||||
(config.getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && Network.get() != Network.MAINNET ? ServerType.BITCOIN_CORE : config.getServerType()) :
|
||||
(config.getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && !PublicElectrumServer.supportedNetwork() ? ServerType.BITCOIN_CORE : config.getServerType()) :
|
||||
(config.getCoreServer() == null && config.getElectrumServer() != null ? ServerType.ELECTRUM_SERVER :
|
||||
(config.getCoreServer() != null || Network.get() != Network.MAINNET ? ServerType.BITCOIN_CORE : ServerType.PUBLIC_ELECTRUM_SERVER));
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
(config.getCoreServer() != null || !PublicElectrumServer.supportedNetwork() ? ServerType.BITCOIN_CORE : ServerType.PUBLIC_ELECTRUM_SERVER));
|
||||
if(!PublicElectrumServer.supportedNetwork()) {
|
||||
serverTypeSegmentedButton.getButtons().remove(publicElectrumToggle);
|
||||
serverTypeToggleGroup.getToggles().remove(publicElectrumToggle);
|
||||
}
|
||||
serverTypeToggleGroup.selectToggle(serverTypeToggleGroup.getToggles().stream().filter(toggle -> toggle.getUserData() == serverType).findFirst().orElse(null));
|
||||
|
||||
publicElectrumServer.setItems(FXCollections.observableList(PublicElectrumServer.getServers()));
|
||||
publicElectrumServer.getSelectionModel().selectedItemProperty().addListener(getPublicElectrumServerListener(config));
|
||||
|
||||
publicUseProxy.selectedProperty().bindBidirectional(useProxy.selectedProperty());
|
||||
@@ -201,6 +215,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
corePort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
|
||||
electrumPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
|
||||
proxyPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
|
||||
coreProxyPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
|
||||
publicProxyPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
|
||||
|
||||
coreHost.textProperty().addListener(getBitcoinCoreListener(config));
|
||||
@@ -209,7 +224,9 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
coreUser.textProperty().addListener(getBitcoinAuthListener(config));
|
||||
corePass.textProperty().addListener(getBitcoinAuthListener(config));
|
||||
|
||||
coreWallet.textProperty().addListener(getBitcoinWalletListener(config));
|
||||
coreUseProxy.selectedProperty().bindBidirectional(useProxy.selectedProperty());
|
||||
coreProxyHost.textProperty().bindBidirectional(proxyHost.textProperty());
|
||||
coreProxyPort.textProperty().bindBidirectional(proxyPort.textProperty());
|
||||
|
||||
electrumHost.textProperty().addListener(getElectrumServerListener(config));
|
||||
electrumPort.textProperty().addListener(getElectrumServerListener(config));
|
||||
@@ -250,9 +267,27 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
}
|
||||
});
|
||||
|
||||
coreMultiWallet.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
config.setCoreMultiWallet(newValue);
|
||||
coreWallet.setDisable(!newValue);
|
||||
recentCoreServers.setConverter(new UrlHostConverter());
|
||||
recentCoreServers.setItems(FXCollections.observableList(Config.get().getRecentCoreServers() == null ? new ArrayList<>() : Config.get().getRecentCoreServers()));
|
||||
recentCoreServers.prefWidthProperty().bind(coreHost.widthProperty());
|
||||
recentCoreServers.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null && Protocol.getProtocol(newValue) != null) {
|
||||
HostAndPort hostAndPort = Protocol.getProtocol(newValue).getServerHostAndPort(newValue);
|
||||
coreHost.setText(hostAndPort.getHost());
|
||||
corePort.setText(Integer.toString(hostAndPort.getPort()));
|
||||
}
|
||||
});
|
||||
|
||||
recentElectrumServers.setConverter(new UrlHostConverter());
|
||||
recentElectrumServers.setItems(FXCollections.observableList(Config.get().getRecentElectrumServers() == null ? new ArrayList<>() : Config.get().getRecentElectrumServers()));
|
||||
recentElectrumServers.prefWidthProperty().bind(electrumHost.widthProperty());
|
||||
recentElectrumServers.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null && Protocol.getProtocol(newValue) != null) {
|
||||
HostAndPort hostAndPort = Protocol.getProtocol(newValue).getServerHostAndPort(newValue);
|
||||
electrumHost.setText(hostAndPort.getHost());
|
||||
electrumPort.setText(Integer.toString(hostAndPort.getPort()));
|
||||
electrumUseSsl.setSelected(Protocol.getProtocol(newValue) == Protocol.SSL);
|
||||
}
|
||||
});
|
||||
|
||||
electrumUseSsl.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
@@ -325,7 +360,8 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
|
||||
PublicElectrumServer configPublicElectrumServer = PublicElectrumServer.fromUrl(config.getPublicElectrumServer());
|
||||
if(configPublicElectrumServer == null) {
|
||||
publicElectrumServer.setValue(PublicElectrumServer.values()[new Random().nextInt(PublicElectrumServer.values().length)]);
|
||||
List<PublicElectrumServer> servers = PublicElectrumServer.getServers();
|
||||
publicElectrumServer.setValue(servers.get(new Random().nextInt(servers.size())));
|
||||
} else {
|
||||
publicElectrumServer.setValue(configPublicElectrumServer);
|
||||
}
|
||||
@@ -358,15 +394,6 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
}
|
||||
}
|
||||
|
||||
coreWallet.setPromptText("Default");
|
||||
if(config.getCoreWallet() == null) {
|
||||
coreWallet.setText(Bwt.DEFAULT_CORE_WALLET);
|
||||
} else {
|
||||
coreWallet.setText(config.getCoreWallet());
|
||||
}
|
||||
|
||||
coreMultiWallet.setSelected(config.getCoreMultiWallet() != Boolean.FALSE);
|
||||
|
||||
String electrumServer = config.getElectrumServer();
|
||||
if(electrumServer != null) {
|
||||
Protocol protocol = Protocol.getProtocol(electrumServer);
|
||||
@@ -519,8 +546,9 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
coreDataDirSelect.setDisable(!editable);
|
||||
coreUser.setDisable(!editable);
|
||||
corePass.setDisable(!editable);
|
||||
coreMultiWallet.setDisable(!editable);
|
||||
coreWallet.setDisable(!editable);
|
||||
coreUseProxy.setDisable(!editable);
|
||||
coreProxyHost.setDisable(!editable);
|
||||
coreProxyPort.setDisable(!editable);
|
||||
|
||||
electrumHost.setDisable(!editable);
|
||||
electrumPort.setDisable(!editable);
|
||||
@@ -665,13 +693,6 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ChangeListener<String> getBitcoinWalletListener(Config config) {
|
||||
return (observable, oldValue, newValue) -> {
|
||||
config.setCoreWallet(coreWallet.getText());
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ChangeListener<String> getElectrumServerListener(Config config) {
|
||||
return (observable, oldValue, newValue) -> {
|
||||
@@ -819,4 +840,16 @@ public class ServerPreferencesController extends PreferencesDetailController {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class UrlHostConverter extends StringConverter<String> {
|
||||
@Override
|
||||
public String toString(String serverUrl) {
|
||||
return serverUrl == null || Protocol.getProtocol(serverUrl) == null ? "" : Protocol.getProtocol(serverUrl).getServerHostAndPort(serverUrl).getHost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.samourai.soroban.cahoots.CahootsContext;
|
||||
import com.samourai.soroban.client.cahoots.OnlineCahootsMessage;
|
||||
import com.samourai.soroban.client.cahoots.SorobanCahootsService;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.samourai.wallet.cahoots.Cahoots;
|
||||
import com.samourai.wallet.cahoots.CahootsType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS;
|
||||
|
||||
public class CounterpartyController extends SorobanController {
|
||||
private static final Logger log = LoggerFactory.getLogger(CounterpartyController.class);
|
||||
|
||||
private String walletId;
|
||||
private Wallet wallet;
|
||||
|
||||
@FXML
|
||||
private VBox step1;
|
||||
|
||||
@FXML
|
||||
private VBox step2;
|
||||
|
||||
@FXML
|
||||
private VBox step3;
|
||||
|
||||
@FXML
|
||||
private VBox step4;
|
||||
|
||||
@FXML
|
||||
private CopyableTextField payNym;
|
||||
|
||||
@FXML
|
||||
private Button showPayNym;
|
||||
|
||||
@FXML
|
||||
private PayNymAvatar payNymAvatar;
|
||||
|
||||
@FXML
|
||||
private Button payNymButton;
|
||||
|
||||
@FXML
|
||||
private PaymentCodeTextField paymentCode;
|
||||
|
||||
@FXML
|
||||
private Button paymentCodeQR;
|
||||
|
||||
@FXML
|
||||
private ComboBox<Wallet> mixWallet;
|
||||
|
||||
@FXML
|
||||
private ProgressTimer step2Timer;
|
||||
|
||||
@FXML
|
||||
private Label step2Desc;
|
||||
|
||||
@FXML
|
||||
private Label mixingPartner;
|
||||
|
||||
@FXML
|
||||
private PayNymAvatar mixPartnerAvatar;
|
||||
|
||||
@FXML
|
||||
private Label meetingFail;
|
||||
|
||||
@FXML
|
||||
private VBox mixDetails;
|
||||
|
||||
@FXML
|
||||
private Label mixType;
|
||||
|
||||
@FXML
|
||||
private Label mixFee;
|
||||
|
||||
@FXML
|
||||
private ProgressTimer step3Timer;
|
||||
|
||||
@FXML
|
||||
private Label step3Desc;
|
||||
|
||||
@FXML
|
||||
private ProgressBar sorobanProgressBar;
|
||||
|
||||
@FXML
|
||||
private Label sorobanProgressLabel;
|
||||
|
||||
@FXML
|
||||
private Glyph mixDeclined;
|
||||
|
||||
@FXML
|
||||
private TransactionDiagram transactionDiagram;
|
||||
|
||||
private final ObjectProperty<Boolean> meetingReceived = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Boolean> meetingAccepted = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Transaction> transactionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
public void initializeView(String walletId, Wallet wallet) {
|
||||
this.walletId = walletId;
|
||||
this.wallet = wallet;
|
||||
|
||||
step1.managedProperty().bind(step1.visibleProperty());
|
||||
step2.managedProperty().bind(step2.visibleProperty());
|
||||
step3.managedProperty().bind(step3.visibleProperty());
|
||||
step4.managedProperty().bind(step4.visibleProperty());
|
||||
|
||||
mixWallet.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Wallet wallet) {
|
||||
return wallet == null ? "" : wallet.getFullDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
mixWallet.setItems(FXCollections.observableList(wallet.getAllWallets()));
|
||||
mixWallet.setValue(wallet);
|
||||
mixWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> setWallet(selectedWallet));
|
||||
|
||||
sorobanProgressBar.managedProperty().bind(sorobanProgressBar.visibleProperty());
|
||||
sorobanProgressLabel.managedProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
mixDeclined.managedProperty().bind(mixDeclined.visibleProperty());
|
||||
sorobanProgressBar.visibleProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
mixDeclined.visibleProperty().bind(sorobanProgressLabel.visibleProperty().not());
|
||||
step2Timer.visibleProperty().bind(mixingPartner.visibleProperty());
|
||||
step3Timer.visibleProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(false);
|
||||
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
if(soroban.getHdWallet() == null) {
|
||||
throw new IllegalStateException("Soroban HD wallet must be set");
|
||||
}
|
||||
|
||||
payNym.managedProperty().bind(payNym.visibleProperty());
|
||||
showPayNym.managedProperty().bind(showPayNym.visibleProperty());
|
||||
showPayNym.visibleProperty().bind(payNym.visibleProperty());
|
||||
payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty());
|
||||
payNymAvatar.visibleProperty().bind(payNym.visibleProperty());
|
||||
payNymButton.managedProperty().bind(payNymButton.visibleProperty());
|
||||
payNymButton.visibleProperty().bind(payNym.visibleProperty().not());
|
||||
if(Config.get().isUsePayNym()) {
|
||||
retrievePayNym(null);
|
||||
} else {
|
||||
payNym.setVisible(false);
|
||||
}
|
||||
|
||||
paymentCode.setPaymentCode(soroban.getPaymentCode());
|
||||
paymentCodeQR.prefHeightProperty().bind(paymentCode.heightProperty());
|
||||
paymentCodeQR.prefWidthProperty().bind(showPayNym.widthProperty());
|
||||
|
||||
mixingPartner.managedProperty().bind(mixingPartner.visibleProperty());
|
||||
meetingFail.managedProperty().bind(meetingFail.visibleProperty());
|
||||
meetingFail.visibleProperty().bind(mixingPartner.visibleProperty().not());
|
||||
|
||||
mixDetails.managedProperty().bind(mixDetails.visibleProperty());
|
||||
mixDetails.setVisible(false);
|
||||
|
||||
meetingAccepted.addListener((observable, oldValue, accepted) -> {
|
||||
Platform.exitNestedEventLoop(meetingAccepted, accepted);
|
||||
meetingReceived.set(null);
|
||||
});
|
||||
|
||||
step2.visibleProperty().addListener((observable, oldValue, visible) -> {
|
||||
if(visible) {
|
||||
startCounterpartyMeetingReceive();
|
||||
step2Timer.start(e -> {
|
||||
step2Desc.setText("Mix declined due to timeout.");
|
||||
meetingReceived.set(Boolean.FALSE);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
step3.visibleProperty().addListener((observable, oldValue, visible) -> {
|
||||
if(visible) {
|
||||
meetingAccepted.set(Boolean.TRUE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setWallet(Wallet wallet) {
|
||||
this.walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
private void startCounterpartyMeetingReceive() {
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
SparrowCahootsWallet counterpartyCahootsWallet = soroban.getCahootsWallet(wallet, 1);
|
||||
|
||||
try {
|
||||
SorobanCahootsService sorobanMeetingService = soroban.getSorobanCahootsService(counterpartyCahootsWallet);
|
||||
sorobanMeetingService.receiveMeetingRequest(TIMEOUT_MS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(requestMessage -> {
|
||||
String code = requestMessage.getSender();
|
||||
CahootsType cahootsType = requestMessage.getType();
|
||||
PaymentCode paymentCodeInitiator = new PaymentCode(code);
|
||||
updateMixPartner(soroban, paymentCodeInitiator, cahootsType);
|
||||
Boolean accepted = (Boolean)Platform.enterNestedEventLoop(meetingAccepted);
|
||||
sorobanMeetingService.sendMeetingResponse(paymentCodeInitiator, requestMessage, accepted)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(responseMessage -> {
|
||||
if(accepted) {
|
||||
startCounterpartyCollaboration(counterpartyCahootsWallet, paymentCodeInitiator, cahootsType);
|
||||
followPaymentCode(soroban, paymentCodeInitiator);
|
||||
}
|
||||
}, error -> {
|
||||
log.error("Error sending meeting response", error);
|
||||
mixingPartner.setVisible(false);
|
||||
});
|
||||
}, error -> {
|
||||
log.error("Failed to receive meeting request", error);
|
||||
mixingPartner.setVisible(false);
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Error sending meeting response", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMixPartner(Soroban soroban, PaymentCode paymentCodeInitiator, CahootsType cahootsType) {
|
||||
String code = paymentCodeInitiator.toString();
|
||||
mixingPartner.setText(code.substring(0, 12) + "..." + code.substring(code.length() - 5));
|
||||
if(Config.get().isUsePayNym()) {
|
||||
mixPartnerAvatar.setPaymentCode(paymentCodeInitiator);
|
||||
soroban.getPayNym(paymentCodeInitiator.toString()).subscribe(payNym -> {
|
||||
mixingPartner.setText(payNym.nymName());
|
||||
}, error -> {
|
||||
//ignore, may not be a PayNym
|
||||
});
|
||||
}
|
||||
|
||||
if(cahootsType == CahootsType.STONEWALLX2) {
|
||||
mixType.setText("Two person coinjoin (" + cahootsType.getLabel() + ")");
|
||||
mixFee.setText("You pay half the miner fee");
|
||||
} else if(cahootsType == CahootsType.STOWAWAY) {
|
||||
mixType.setText("Payjoin (" + cahootsType.getLabel() + ")");
|
||||
mixFee.setText("None");
|
||||
} else {
|
||||
mixType.setText(cahootsType.getLabel());
|
||||
mixFee.setText("None");
|
||||
}
|
||||
|
||||
mixDetails.setVisible(true);
|
||||
meetingReceived.set(Boolean.TRUE);
|
||||
}
|
||||
|
||||
private void startCounterpartyCollaboration(SparrowCahootsWallet counterpartyCahootsWallet, PaymentCode initiatorPaymentCode, CahootsType cahootsType) {
|
||||
sorobanProgressLabel.setText("Creating mix transaction...");
|
||||
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getWalletUtxos();
|
||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : walletUtxos.entrySet()) {
|
||||
counterpartyCahootsWallet.addUtxo(wallet, entry.getValue(), wallet.getTransactions().get(entry.getKey().getHash()), (int)entry.getKey().getIndex());
|
||||
}
|
||||
|
||||
try {
|
||||
SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(counterpartyCahootsWallet);
|
||||
CahootsContext cahootsContext = cahootsType == CahootsType.STONEWALLX2 ? CahootsContext.newCounterpartyStonewallx2() : CahootsContext.newCounterpartyStowaway();
|
||||
sorobanCahootsService.contributor(counterpartyCahootsWallet.getAccount(), cahootsContext, initiatorPaymentCode, TIMEOUT_MS)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(sorobanMessage -> {
|
||||
OnlineCahootsMessage cahootsMessage = (OnlineCahootsMessage)sorobanMessage;
|
||||
if(cahootsMessage != null) {
|
||||
Cahoots cahoots = cahootsMessage.getCahoots();
|
||||
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5);
|
||||
|
||||
if(cahoots.getStep() == 3) {
|
||||
sorobanProgressLabel.setText("Your mix partner is reviewing the transaction...");
|
||||
step3Timer.start();
|
||||
} else if(cahoots.getStep() >= 4) {
|
||||
try {
|
||||
Transaction transaction = getTransaction(cahoots);
|
||||
if(transaction != null) {
|
||||
transactionProperty.set(transaction);
|
||||
updateTransactionDiagram(transactionDiagram, wallet, null, transaction);
|
||||
next();
|
||||
}
|
||||
} catch(PSBTParseException e) {
|
||||
log.error("Invalid collaborative PSBT created", e);
|
||||
step3Desc.setText("Invalid transaction created.");
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, error -> {
|
||||
log.error("Error creating mix transaction", error);
|
||||
String cutFrom = "Exception: ";
|
||||
int index = error.getMessage().lastIndexOf(cutFrom);
|
||||
String msg = index < 0 ? error.getMessage() : error.getMessage().substring(index + cutFrom.length());
|
||||
msg = msg.replace("#Cahoots", "mix transaction");
|
||||
step3Desc.setText(msg);
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating mix transaction", e);
|
||||
sorobanProgressLabel.setText(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void followPaymentCode(Soroban soroban, PaymentCode paymentCodeInitiator) {
|
||||
if(Config.get().isUsePayNym() && soroban.getHdWallet() != null) {
|
||||
soroban.getAuthToken(new HashMap<>()).subscribe(authToken -> {
|
||||
String signature = soroban.getSignature(authToken);
|
||||
soroban.followPaymentCode(paymentCodeInitiator, authToken, signature).subscribe(followMap -> {
|
||||
log.debug("Followed payment code " + followMap.get("following"));
|
||||
}, error -> {
|
||||
log.warn("Could not follow payment code", error);
|
||||
});
|
||||
}, error -> {
|
||||
log.warn("Could not follow payment code", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public boolean next() {
|
||||
if(step1.isVisible()) {
|
||||
step1.setVisible(false);
|
||||
step2.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step2.isVisible()) {
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step3.isVisible()) {
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
meetingAccepted.set(Boolean.FALSE);
|
||||
}
|
||||
|
||||
public void retrievePayNym(ActionEvent event) {
|
||||
Config.get().setUsePayNym(true);
|
||||
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
soroban.createPayNym().subscribe(createMap -> {
|
||||
payNym.setText((String)createMap.get("nymName"));
|
||||
payNymAvatar.setPaymentCode(soroban.getPaymentCode());
|
||||
payNym.setVisible(true);
|
||||
|
||||
claimPayNym(soroban, createMap);
|
||||
}, error -> {
|
||||
log.error("Error retrieving PayNym", error);
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
retrievePayNym(null);
|
||||
} else {
|
||||
payNym.setVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void showPayNym(ActionEvent event) {
|
||||
PayNymDialog payNymDialog = new PayNymDialog(walletId, false);
|
||||
payNymDialog.showAndWait();
|
||||
}
|
||||
|
||||
public void showPayNymQR(ActionEvent event) {
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(soroban.getPaymentCode().toString());
|
||||
qrDisplayDialog.showAndWait();
|
||||
}
|
||||
|
||||
public ObjectProperty<Boolean> meetingReceivedProperty() {
|
||||
return meetingReceived;
|
||||
}
|
||||
|
||||
public ObjectProperty<Boolean> meetingAcceptedProperty() {
|
||||
return meetingAccepted;
|
||||
}
|
||||
|
||||
public ObjectProperty<Transaction> transactionProperty() {
|
||||
return transactionProperty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CounterpartyDialog extends Dialog<Boolean> {
|
||||
public CounterpartyDialog(String walletId, Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader counterpartyLoader = new FXMLLoader(AppServices.class.getResource("soroban/counterparty.fxml"));
|
||||
dialogPane.setContent(counterpartyLoader.load());
|
||||
CounterpartyController counterpartyController = counterpartyLoader.getController();
|
||||
counterpartyController.initializeView(walletId, wallet);
|
||||
|
||||
dialogPane.setPrefWidth(730);
|
||||
dialogPane.setPrefHeight(520);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/counterparty.css").toExternalForm());
|
||||
|
||||
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.APPLY);
|
||||
dialogPane.getButtonTypes().addAll(nextButtonType, cancelButtonType, doneButtonType);
|
||||
|
||||
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
|
||||
Button cancelButton = (Button)dialogPane.lookupButton(cancelButtonType);
|
||||
Button doneButton = (Button)dialogPane.lookupButton(doneButtonType);
|
||||
doneButton.setDisable(true);
|
||||
counterpartyController.meetingReceivedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
nextButton.setDisable(newValue != Boolean.TRUE);
|
||||
});
|
||||
counterpartyController.transactionProperty().addListener((observable, oldValue, newValue) -> {
|
||||
nextButton.setVisible(false);
|
||||
doneButton.setDisable(newValue == null);
|
||||
cancelButton.setDisable(newValue != null);
|
||||
});
|
||||
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
doneButton.managedProperty().bind(doneButton.visibleProperty());
|
||||
|
||||
doneButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
|
||||
nextButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
if(!counterpartyController.next()) {
|
||||
nextButton.setVisible(false);
|
||||
doneButton.setDefaultButton(true);
|
||||
}
|
||||
nextButton.setDisable(counterpartyController.meetingReceivedProperty().get() != Boolean.TRUE);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
cancelButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
if(counterpartyController.meetingReceivedProperty().get() == Boolean.TRUE) {
|
||||
counterpartyController.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton.getButtonData().equals(ButtonBar.ButtonData.APPLY));
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user