mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 12:06:15 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67dcf69a78 | ||
|
|
7ad8a04bda | ||
|
|
24e75603c6 | ||
|
|
545342dfb4 | ||
|
|
b15d6308bd | ||
|
|
ff0c381437 | ||
|
|
555260e954 | ||
|
|
8d584d1c48 | ||
|
|
967cf0cdfa | ||
|
|
41ba8455a0 | ||
|
|
d84f3bf887 | ||
|
|
153815d9e3 | ||
|
|
0250579445 | ||
|
|
7590d786b5 | ||
|
|
55809b7dc3 | ||
|
|
06026b0a09 | ||
|
|
2cd64aa650 | ||
|
|
0b980f6ab5 | ||
|
|
73dcef9fd1 | ||
|
|
4e3491ec64 | ||
|
|
176e440195 | ||
|
|
300545b289 | ||
|
|
057a9efb1f | ||
|
|
9edeff9aab | ||
|
|
f938506a3f | ||
|
|
4fb8c5a61b | ||
|
|
7a99c4a11a | ||
|
|
6c13504644 | ||
|
|
3ddf4ed4b2 | ||
|
|
6b59ff60ad | ||
|
|
7c64d689fd | ||
|
|
4ad9cdedb6 | ||
|
|
276cb8aecb | ||
|
|
e7ed82699c | ||
|
|
68cd3673af | ||
|
|
5f96570c07 | ||
|
|
5147ee8aee | ||
|
|
3cc2981b72 | ||
|
|
8038298485 | ||
|
|
d1a1bd5751 | ||
|
|
63b7aef91e | ||
|
|
8a51a47156 | ||
|
|
827efe071e | ||
|
|
56784b684a | ||
|
|
1fa52f043c | ||
|
|
ce44cfe877 | ||
|
|
8ba0a9f360 | ||
|
|
41dabac75b | ||
|
|
064708f088 | ||
|
|
66dc394215 | ||
|
|
5ca60699ef | ||
|
|
61d9ad1875 | ||
|
|
af6bbebac4 | ||
|
|
6f4fc4f2ca | ||
|
|
00f5001385 | ||
|
|
3f3cdca94f | ||
|
|
08cf01a5c6 | ||
|
|
df7f40dbc9 | ||
|
|
12c1725260 | ||
|
|
aa8380eb03 | ||
|
|
0e26f8fce1 | ||
|
|
8de14dcbce | ||
|
|
6871810c7c | ||
|
|
6ac294920e | ||
|
|
4b32eb397e | ||
|
|
b25297e8b9 | ||
|
|
ff90a2c3e6 | ||
|
|
3cbe8d1537 | ||
|
|
9293b622a3 | ||
|
|
6337e1cf7d | ||
|
|
3ff3fb29b0 | ||
|
|
149d297193 | ||
|
|
47f7b8870c | ||
|
|
7ce7d37da7 | ||
|
|
b422c754d6 | ||
|
|
be6e9019dc | ||
|
|
011bb86b5f | ||
|
|
8e1163d3db | ||
|
|
83c8b1c8e6 | ||
|
|
d44aecea90 | ||
|
|
c9288ab25b | ||
|
|
e39a2cb944 | ||
|
|
fb25edb51c | ||
|
|
06ff0498d4 | ||
|
|
63b27e7054 | ||
|
|
0260a12663 | ||
|
|
a05fcba6d9 | ||
|
|
5be5363f25 | ||
|
|
cc961b4eeb | ||
|
|
7e7795196c | ||
|
|
fd0fe1110d | ||
|
|
ea64fa0f85 | ||
|
|
2972f1a4d7 | ||
|
|
6990b398c2 | ||
|
|
a25b53bd44 | ||
|
|
871c503bc9 | ||
|
|
b7992ae9e1 | ||
|
|
0a8eb2fbb7 | ||
|
|
7863fb7632 | ||
|
|
6481d83b0c | ||
|
|
402d9b14ec | ||
|
|
feeb4f0ac0 | ||
|
|
143eb3213e | ||
|
|
cd96fc1daa | ||
|
|
73d9cd2e68 | ||
|
|
3faf817148 | ||
|
|
96c88b7472 | ||
|
|
e2795c7ef3 |
+41
-17
@@ -7,7 +7,7 @@ plugins {
|
||||
id 'org.beryx.jlink' version '2.25.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.7.0'
|
||||
def sparrowVersion = '1.7.2'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -54,7 +54,7 @@ dependencies {
|
||||
exclude group: 'org.hamcrest'
|
||||
exclude group: 'junit'
|
||||
}
|
||||
implementation('com.google.guava:guava:28.2-jre')
|
||||
implementation('com.google.guava:guava:31.1-jre')
|
||||
implementation('com.google.code.gson:gson:2.8.6')
|
||||
implementation('com.h2database:h2:2.1.214')
|
||||
implementation('com.zaxxer:HikariCP:4.0.3')
|
||||
@@ -69,11 +69,11 @@ dependencies {
|
||||
exclude group: 'com.beust', module: 'jcommander'
|
||||
}
|
||||
implementation('com.beust:jcommander:1.81')
|
||||
implementation('com.github.arteam:simple-json-rpc-core:1.0')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.0') {
|
||||
implementation('com.github.arteam:simple-json-rpc-core:1.3')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.3') {
|
||||
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
||||
}
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.4')
|
||||
@@ -103,14 +103,17 @@ dependencies {
|
||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.33')
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.34')
|
||||
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')
|
||||
implementation('com.github.librepdf:openpdf:1.3.27')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.1')
|
||||
testImplementation('junit:junit:4.12')
|
||||
implementation('net.coobird:thumbnailator:0.4.18')
|
||||
implementation('com.github.hervegirod:fxsvgimage:1.0b2')
|
||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||
testImplementation('junit:junit:4.13.1')
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -158,7 +161,8 @@ run {
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson"]
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow"]
|
||||
|
||||
if(os.macOsX) {
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
||||
@@ -207,6 +211,7 @@ jlink {
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.sql",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
|
||||
@@ -216,6 +221,9 @@ jlink {
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
|
||||
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor"]
|
||||
|
||||
if(os.windows) {
|
||||
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
|
||||
}
|
||||
if(os.macOsX) {
|
||||
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
|
||||
}
|
||||
@@ -237,7 +245,12 @@ jlink {
|
||||
installerType = "exe"
|
||||
}
|
||||
if(os.linux) {
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow', '--linux-rpm-license-type', 'ASL 2.0']
|
||||
if(headless) {
|
||||
installerOptions = ['--license-file', 'LICENSE']
|
||||
} else {
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||
}
|
||||
installerOptions += ['--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
||||
}
|
||||
if(os.macOsX) {
|
||||
@@ -272,7 +285,7 @@ task packageTarDistribution(type: Tar) {
|
||||
}
|
||||
|
||||
extraJavaModuleInfo {
|
||||
module('jackson-core-2.10.1.jar', 'com.fasterxml.jackson.core', '2.10.1') {
|
||||
module('jackson-core-2.13.2.jar', 'com.fasterxml.jackson.core', '2.13.2') {
|
||||
exports('com.fasterxml.jackson.core')
|
||||
exports('com.fasterxml.jackson.core.async')
|
||||
exports('com.fasterxml.jackson.core.base')
|
||||
@@ -287,11 +300,11 @@ extraJavaModuleInfo {
|
||||
exports('com.fasterxml.jackson.core.util')
|
||||
uses('com.fasterxml.jackson.core.ObjectCodec')
|
||||
}
|
||||
module('jackson-annotations-2.10.1.jar', 'com.fasterxml.jackson.annotation', '2.10.1') {
|
||||
module('jackson-annotations-2.13.2.jar', 'com.fasterxml.jackson.annotation', '2.13.2') {
|
||||
requires('com.fasterxml.jackson.core')
|
||||
exports('com.fasterxml.jackson.annotation')
|
||||
}
|
||||
module('jackson-databind-2.10.1.jar', 'com.fasterxml.jackson.databind', '2.10.1') {
|
||||
module('jackson-databind-2.13.2.jar', 'com.fasterxml.jackson.databind', '2.13.2') {
|
||||
requires('java.desktop')
|
||||
requires('java.logging')
|
||||
requires('com.fasterxml.jackson.annotation')
|
||||
@@ -325,7 +338,7 @@ extraJavaModuleInfo {
|
||||
exports('tornadofx.control')
|
||||
requires('javafx.controls')
|
||||
}
|
||||
module('simple-json-rpc-core-1.0.jar', 'simple.json.rpc.core', '1.0') {
|
||||
module('simple-json-rpc-core-1.3.jar', 'simple.json.rpc.core', '1.3') {
|
||||
exports('com.github.arteam.simplejsonrpc.core.annotation')
|
||||
exports('com.github.arteam.simplejsonrpc.core.domain')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
@@ -333,7 +346,7 @@ extraJavaModuleInfo {
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('org.jetbrains.annotations')
|
||||
}
|
||||
module('simple-json-rpc-client-1.0.jar', 'simple.json.rpc.client', '1.0') {
|
||||
module('simple-json-rpc-client-1.3.jar', 'simple.json.rpc.client', '1.3') {
|
||||
exports('com.github.arteam.simplejsonrpc.client')
|
||||
exports('com.github.arteam.simplejsonrpc.client.builder')
|
||||
exports('com.github.arteam.simplejsonrpc.client.exception')
|
||||
@@ -341,7 +354,7 @@ extraJavaModuleInfo {
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('simple.json.rpc.core')
|
||||
}
|
||||
module('simple-json-rpc-server-1.0.jar', 'simple.json.rpc.server', '1.0') {
|
||||
module('simple-json-rpc-server-1.3.jar', 'simple.json.rpc.server', '1.3') {
|
||||
exports('com.github.arteam.simplejsonrpc.server')
|
||||
requires('simple.json.rpc.core')
|
||||
requires('com.google.common')
|
||||
@@ -397,7 +410,7 @@ extraJavaModuleInfo {
|
||||
exports('com.google.common.util.concurrent.internal')
|
||||
}
|
||||
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
|
||||
module('guava-28.2-jre.jar', 'com.google.common', '28.2-jre') {
|
||||
module('guava-31.1-jre.jar', 'com.google.common', '31.1-jre') {
|
||||
exports('com.google.common.eventbus')
|
||||
exports('com.google.common.net')
|
||||
exports('com.google.common.base')
|
||||
@@ -482,8 +495,10 @@ extraJavaModuleInfo {
|
||||
}
|
||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||
exports('co.nstant.in.cbor')
|
||||
exports('co.nstant.in.cbor.model')
|
||||
exports('co.nstant.in.cbor.builder')
|
||||
}
|
||||
module('nightjar-0.2.33.jar', 'com.sparrowwallet.nightjar', '0.2.33') {
|
||||
module('nightjar-0.2.34.jar', 'com.sparrowwallet.nightjar', '0.2.34') {
|
||||
requires('com.google.common')
|
||||
requires('net.sourceforge.streamsupport')
|
||||
requires('org.slf4j')
|
||||
@@ -571,6 +586,15 @@ extraJavaModuleInfo {
|
||||
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
|
||||
exports('net.jcip.annotations')
|
||||
}
|
||||
module('thumbnailator-0.4.18.jar', 'net.coobird.thumbnailator', '0.4.18') {
|
||||
exports('net.coobird.thumbnailator')
|
||||
requires('java.desktop')
|
||||
}
|
||||
module('fxsvgimage-1.0b2.jar', 'com.github.hervegirod', '1.0b2') {
|
||||
exports('org.girod.javafx.svgimage')
|
||||
requires('javafx.graphics')
|
||||
requires('java.xml')
|
||||
}
|
||||
module("netlayer-jpms-${osName}${targetName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
|
||||
exports('org.berndpruenster.netlayer.tor')
|
||||
requires('com.github.ravn.jsocks')
|
||||
|
||||
+27
-4
@@ -79,16 +79,39 @@ sudo apt install -y rpm fakeroot binutils
|
||||
|
||||
### Building the binaries
|
||||
|
||||
The project can cloned for a specific release tag as follows:
|
||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||
|
||||
```shell
|
||||
GIT_TAG="1.7.2"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
||||
```shell
|
||||
GIT_TAG="1.7.0"
|
||||
git clone --recursive --branch "${GIT_TAG}" https://github.com/sparrowwallet/sparrow.git
|
||||
```
|
||||
|
||||
Thereafter, building should be straightforward:
|
||||
If you already have the sparrow repo cloned, fetch all new updates and checkout the release. For this, change into your local sparrow folder and execute:
|
||||
|
||||
```shell
|
||||
cd sparrow
|
||||
cd {yourPathToSparrow}/sparrow
|
||||
git pull --recurse-submodules
|
||||
git checkout "${GIT_TAG}"
|
||||
```
|
||||
|
||||
Note - there is an additional step if you updated rather than initially cloned your repo at `GIT_TAG`.
|
||||
This is due to the [drongo submodule](https://github.com/sparrowwallet/drongo/tree/master) which needs to be checked out to the commit state it had at the time of the release.
|
||||
Only then your build will be comparable to the provided one in the release section of Github.
|
||||
To checkout the submodule to the correct commit for `GIT_TAG`, additionally run:
|
||||
|
||||
```shell
|
||||
git submodule update --checkout
|
||||
```
|
||||
|
||||
Thereafter, building should be straightforward. If not already done, change into the sparrow folder and run:
|
||||
|
||||
```shell
|
||||
cd {yourPathToSparrow}/sparrow # if you aren't already in the sparrow folder
|
||||
./gradlew jpackage
|
||||
```
|
||||
|
||||
|
||||
+1
-1
Submodule drongo updated: 7c34ec7c3b...d48054ac6b
Vendored
BIN
Binary file not shown.
+2
-1
@@ -1,5 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -80,10 +80,10 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
@@ -143,12 +143,16 @@ fi
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
|
||||
Vendored
+1
@@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@ Exec=/opt/sparrow/bin/Sparrow %U
|
||||
Icon=/opt/sparrow/lib/Sparrow.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Unknown
|
||||
Categories=Finance;Network;
|
||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.7.0</string>
|
||||
<string>1.7.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -222,6 +222,41 @@ public class AppController implements Initializable {
|
||||
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
|
||||
};
|
||||
|
||||
private final ListChangeListener<Tab> tabsChangeListener = (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) {
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
|
||||
}
|
||||
|
||||
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().filter(tab -> tab.getUserData() instanceof WalletTabData)
|
||||
.flatMap(tab -> ((TabPane) tab.getContent()).getTabs().stream().map(subTab -> (WalletTabData) subTab.getUserData())).collect(Collectors.toList());
|
||||
if(!closedWalletTabs.isEmpty()) {
|
||||
EventManager.get().post(new WalletTabsClosedEvent(closedWalletTabs));
|
||||
}
|
||||
|
||||
List<TransactionTabData> closedTransactionTabs = c.getRemoved().stream().map(tab -> (TabData) tab.getUserData())
|
||||
.filter(tabData -> tabData.getType() == TabData.TabType.TRANSACTION).map(tabData -> (TransactionTabData) tabData).collect(Collectors.toList());
|
||||
if(!closedTransactionTabs.isEmpty()) {
|
||||
EventManager.get().post(new TransactionTabsClosedEvent(closedTransactionTabs));
|
||||
}
|
||||
|
||||
closeTab.setDisable(tabs.getTabs().isEmpty());
|
||||
if(tabs.getTabs().isEmpty()) {
|
||||
Stage tabStage = (Stage) tabs.getScene().getWindow();
|
||||
tabStage.setTitle("Sparrow");
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(true);
|
||||
exportWallet.setDisable(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
@@ -278,40 +313,7 @@ public class AppController implements Initializable {
|
||||
|
||||
//Draggle tabs introduce unwanted movement when selecting between them
|
||||
//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) {
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
|
||||
}
|
||||
|
||||
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().filter(tab -> tab.getUserData() instanceof WalletTabData)
|
||||
.flatMap(tab -> ((TabPane)tab.getContent()).getTabs().stream().map(subTab -> (WalletTabData)subTab.getUserData())).collect(Collectors.toList());
|
||||
if(!closedWalletTabs.isEmpty()) {
|
||||
EventManager.get().post(new WalletTabsClosedEvent(closedWalletTabs));
|
||||
}
|
||||
|
||||
List<TransactionTabData> closedTransactionTabs = c.getRemoved().stream().map(tab -> (TabData)tab.getUserData())
|
||||
.filter(tabData -> tabData.getType() == TabData.TabType.TRANSACTION).map(tabData -> (TransactionTabData)tabData).collect(Collectors.toList());
|
||||
if(!closedTransactionTabs.isEmpty()) {
|
||||
EventManager.get().post(new TransactionTabsClosedEvent(closedTransactionTabs));
|
||||
}
|
||||
|
||||
closeTab.setDisable(tabs.getTabs().isEmpty());
|
||||
if(tabs.getTabs().isEmpty()) {
|
||||
Stage tabStage = (Stage)tabs.getScene().getWindow();
|
||||
tabStage.setTitle("Sparrow");
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(true);
|
||||
exportWallet.setDisable(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
tabs.getTabs().addListener(tabsChangeListener);
|
||||
|
||||
tabs.getScene().getWindow().setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
@@ -384,7 +386,7 @@ public class AppController implements Initializable {
|
||||
configureSwitchServer();
|
||||
setServerType(Config.get().getServerType());
|
||||
serverToggle.setSelected(isConnected());
|
||||
serverToggle.setDisable(Config.get().getServerType() == null);
|
||||
serverToggle.setDisable(!Config.get().hasServer());
|
||||
onlineProperty().bindBidirectional(serverToggle.selectedProperty());
|
||||
onlineProperty().addListener(new WeakChangeListener<>(serverToggleOnlineListener));
|
||||
serverToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
@@ -942,6 +944,8 @@ public class AppController implements Initializable {
|
||||
if(AppServices.isConnected()) {
|
||||
return "Connected to " + Config.get().getServerDisplayName() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") +
|
||||
(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? "\nWarning! You are connected to a public server and sharing your transaction data with it.\nFor better privacy, consider using your own Bitcoin Core node or private Electrum server." : "");
|
||||
} else if(AppServices.isConnecting()) {
|
||||
return "Connecting...";
|
||||
}
|
||||
|
||||
return "Disconnected";
|
||||
@@ -1064,11 +1068,14 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
public void importWallet(ActionEvent event) {
|
||||
WalletImportDialog dlg = new WalletImportDialog();
|
||||
List<WalletForm> selectedWalletForms = getSelectedWalletForms();
|
||||
WalletImportDialog dlg = new WalletImportDialog(selectedWalletForms);
|
||||
Optional<Wallet> optionalWallet = dlg.showAndWait();
|
||||
if(optionalWallet.isPresent()) {
|
||||
Wallet wallet = optionalWallet.get();
|
||||
addImportedWallet(wallet);
|
||||
if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) {
|
||||
addImportedWallet(wallet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1241,6 +1248,7 @@ public class AppController implements Initializable {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(preferenceGroup);
|
||||
preferencesDialog.showAndWait();
|
||||
configureSwitchServer();
|
||||
serverToggle.setDisable(!Config.get().hasServer());
|
||||
}
|
||||
|
||||
public void signVerifyMessage(ActionEvent event) {
|
||||
@@ -1461,11 +1469,10 @@ public class AppController implements Initializable {
|
||||
wallet.setName(name);
|
||||
}
|
||||
Tab tab = new Tab("");
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
|
||||
glyph.setFontSize(10.0);
|
||||
glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
|
||||
WalletIcon walletIcon = new WalletIcon(storage, wallet);
|
||||
walletIcon.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
|
||||
Label tabLabel = new Label(name);
|
||||
tabLabel.setGraphic(glyph);
|
||||
tabLabel.setGraphic(walletIcon);
|
||||
tabLabel.setGraphicTextGap(5.0);
|
||||
tab.setGraphic(tabLabel);
|
||||
tab.setClosable(true);
|
||||
@@ -1597,7 +1604,7 @@ 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));
|
||||
subTab.setContextMenu(getSubTabContextMenu(storage, wallet, subTabs, subTab));
|
||||
}
|
||||
|
||||
subTabs.getTabs().add(subTab);
|
||||
@@ -1608,6 +1615,10 @@ public class AppController implements Initializable {
|
||||
});
|
||||
subTabs.getSelectionModel().select(subTab);
|
||||
|
||||
if(wallet.isValid()) {
|
||||
Platform.runLater(() -> walletForm.refreshHistory(AppServices.getCurrentBlockHeight()));
|
||||
}
|
||||
|
||||
return walletForm;
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -1646,6 +1657,19 @@ public class AppController implements Initializable {
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<WalletForm> getSelectedWalletForms() {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
if(selectedTab != null) {
|
||||
TabData tabData = (TabData) selectedTab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane) selectedTab.getContent();
|
||||
return subTabs.getTabs().stream().map(subTab -> ((WalletTabData) subTab.getUserData()).getWalletForm()).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -1813,15 +1837,19 @@ public class AppController implements Initializable {
|
||||
MenuItem moveRight = new MenuItem("Move Right");
|
||||
moveRight.setOnAction(event -> {
|
||||
int index = tabs.getTabs().indexOf(tab);
|
||||
tabs.getTabs().removeListener(tabsChangeListener);
|
||||
tabs.getTabs().remove(tab);
|
||||
tabs.getTabs().add(index + 1, tab);
|
||||
tabs.getTabs().addListener(tabsChangeListener);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
});
|
||||
MenuItem moveLeft = new MenuItem("Move Left");
|
||||
moveLeft.setOnAction(event -> {
|
||||
int index = tabs.getTabs().indexOf(tab);
|
||||
tabs.getTabs().removeListener(tabsChangeListener);
|
||||
tabs.getTabs().remove(tab);
|
||||
tabs.getTabs().add(index - 1, tab);
|
||||
tabs.getTabs().addListener(tabsChangeListener);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
});
|
||||
contextMenu.getItems().addAll(moveRight, moveLeft);
|
||||
@@ -1850,17 +1878,61 @@ public class AppController implements Initializable {
|
||||
|
||||
contextMenu.getItems().addAll(new SeparatorMenuItem(), close, closeOthers, closeAll);
|
||||
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
if(tab.getUserData() instanceof WalletTabData walletTabData) {
|
||||
Menu walletIcon = new Menu("Wallet Icon");
|
||||
MenuItem custom = new MenuItem("Custom...");
|
||||
custom.setOnAction(event -> {
|
||||
setCustomIcon(walletTabData.getWallet());
|
||||
});
|
||||
MenuItem reset = new MenuItem("Reset");
|
||||
reset.setOnAction(event -> {
|
||||
resetIcon(walletTabData.getWalletForm());
|
||||
});
|
||||
walletIcon.getItems().addAll(custom, reset);
|
||||
|
||||
MenuItem delete = new MenuItem("Delete...");
|
||||
delete.setOnAction(event -> {
|
||||
deleteWallet(getSelectedWalletForm());
|
||||
deleteWallet(walletTabData.getWalletForm());
|
||||
});
|
||||
contextMenu.getItems().addAll(new SeparatorMenuItem(), delete);
|
||||
contextMenu.getItems().addAll(new SeparatorMenuItem(), walletIcon, delete);
|
||||
}
|
||||
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
private void setCustomIcon(Wallet wallet) {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Image");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("Images", "*.png", "*.jpg", "*.jpeg", "*.gif")
|
||||
);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
if(file != null) {
|
||||
try {
|
||||
byte[] iconData = ImageUtils.resize(file, WalletIcon.SAVE_WIDTH, WalletIcon.SAVE_HEIGHT);
|
||||
WalletConfig walletConfig = wallet.getMasterWalletConfig();
|
||||
walletConfig.setIconData(iconData, true);
|
||||
EventManager.get().post(new WalletConfigChangedEvent(wallet));
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating custom wallet icon", e);
|
||||
showErrorDialog("Error creating custom wallet icon", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void resetIcon(WalletForm walletForm) {
|
||||
Wallet masterWallet = walletForm.getMasterWallet();
|
||||
if(masterWallet.getWalletConfig() != null && masterWallet.getWalletConfig().isUserIcon()) {
|
||||
masterWallet.getWalletConfig().setIconData(null, false);
|
||||
EventManager.get().post(new WalletConfigChangedEvent(masterWallet));
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteWallet(WalletForm selectedWalletForm) {
|
||||
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Delete " + selectedWalletForm.getWallet().getMasterName() + "?", "The wallet file and any backups will be deleted. Are you sure?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
@@ -1905,13 +1977,30 @@ public class AppController implements Initializable {
|
||||
|
||||
private void deleteStorage(Storage storage) {
|
||||
if(storage.isClosed()) {
|
||||
Platform.runLater(storage::delete);
|
||||
Platform.runLater(() -> {
|
||||
Storage.DeleteWalletService deleteWalletService = new Storage.DeleteWalletService(storage);
|
||||
deleteWalletService.setDelay(Duration.seconds(3));
|
||||
deleteWalletService.setPeriod(Duration.hours(1));
|
||||
deleteWalletService.setOnSucceeded(event -> {
|
||||
deleteWalletService.cancel();
|
||||
if(!deleteWalletService.getValue()) {
|
||||
showErrorDialog("Error deleting wallet", "Could not delete " + storage.getWalletFile().getName() + ". Please delete this file manually.");
|
||||
}
|
||||
});
|
||||
deleteWalletService.setOnFailed(event -> {
|
||||
deleteWalletService.cancel();
|
||||
showErrorDialog("Error deleting wallet", "Could not delete " + storage.getWalletFile().getName() + ". Please delete this file manually.");
|
||||
});
|
||||
deleteWalletService.start();
|
||||
});
|
||||
} else {
|
||||
Platform.runLater(() -> deleteStorage(storage));
|
||||
}
|
||||
}
|
||||
|
||||
private ContextMenu getSubTabContextMenu(Wallet wallet, TabPane subTabs, Tab subTab) {
|
||||
private ContextMenu getSubTabContextMenu(Storage storage, Wallet wallet, TabPane subTabs, Tab subTab) {
|
||||
String walletId = storage.getWalletId(wallet);
|
||||
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
MenuItem rename = new MenuItem("Rename Account");
|
||||
rename.setOnAction(event -> {
|
||||
@@ -1922,8 +2011,9 @@ public class AppController implements Initializable {
|
||||
String label = optLabel.get();
|
||||
subTabLabel.setText(label);
|
||||
|
||||
wallet.setLabel(label);
|
||||
EventManager.get().post(new WalletLabelChangedEvent(wallet));
|
||||
Wallet renamedWallet = AppServices.get().getWallet(walletId);
|
||||
renamedWallet.setLabel(label);
|
||||
EventManager.get().post(new WalletLabelChangedEvent(renamedWallet));
|
||||
}
|
||||
});
|
||||
contextMenu.getItems().add(rename);
|
||||
@@ -1933,11 +2023,12 @@ public class AppController implements Initializable {
|
||||
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) {
|
||||
Wallet deletedWallet = AppServices.get().getWallet(walletId);
|
||||
EventManager.get().post(new WalletDeletedEvent(deletedWallet));
|
||||
subTabs.getTabs().remove(subTab);
|
||||
if(subTabs.getTabs().size() == 1) {
|
||||
setSubTabsVisible(subTabs, areSubTabsVisible());
|
||||
}
|
||||
EventManager.get().post(new WalletDeletedEvent(wallet));
|
||||
}
|
||||
});
|
||||
contextMenu.getItems().add(delete);
|
||||
@@ -2001,7 +2092,7 @@ public class AppController implements Initializable {
|
||||
serverToggle.getStyleClass().remove("core-server");
|
||||
}
|
||||
|
||||
serverToggle.setDisable(false);
|
||||
serverToggle.setDisable(!Config.get().hasServer());
|
||||
}
|
||||
|
||||
public void setTheme(ActionEvent event) {
|
||||
@@ -2050,8 +2141,8 @@ public class AppController implements Initializable {
|
||||
|
||||
private void tabLabelAddFailure(Tab tab) {
|
||||
Label tabLabel = (Label)tab.getGraphic();
|
||||
if(!tabLabel.getStyleClass().contains("failure")) {
|
||||
tabLabel.getGraphic().getStyleClass().add("failure");
|
||||
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
|
||||
if(walletIcon.addFailure()) {
|
||||
tabLabel.setTooltip(new Tooltip("Error loading transaction history from server"));
|
||||
}
|
||||
}
|
||||
@@ -2084,7 +2175,8 @@ public class AppController implements Initializable {
|
||||
|
||||
private void tabLabelRemoveFailure(Tab tab) {
|
||||
Label tabLabel = (Label)tab.getGraphic();
|
||||
tabLabel.getGraphic().getStyleClass().remove("failure");
|
||||
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
|
||||
walletIcon.removeFailure();
|
||||
tabLabel.setTooltip(null);
|
||||
}
|
||||
|
||||
@@ -2121,6 +2213,14 @@ public class AppController implements Initializable {
|
||||
} else {
|
||||
tabs.getScene().getStylesheets().remove(darkCss);
|
||||
}
|
||||
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
Label tabLabel = (Label)tab.getGraphic();
|
||||
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
|
||||
walletIcon.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -2201,6 +2301,9 @@ public class AppController implements Initializable {
|
||||
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(event.getWallet().getLabel() != null ? event.getWallet().getLabel() : event.getWallet().getAutomaticName());
|
||||
Label tabLabel = (Label)walletTab.getGraphic();
|
||||
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
|
||||
walletIcon.setWallet(event.getWallet());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2511,6 +2614,50 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void cormorantSyncStatus(CormorantSyncStatusEvent event) {
|
||||
serverToggle.setDisable(false);
|
||||
if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) {
|
||||
statusUpdated(new StatusEvent("Syncing... (" + event.getProgress() + "% complete, synced to " + event.getTipAsString() + ")"));
|
||||
if(event.getProgress() > 0 && (statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING)) {
|
||||
statusBar.setProgress((double)event.getProgress() / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void cormorantScanStatus(CormorantScanStatusEvent event) {
|
||||
serverToggle.setDisable(true);
|
||||
if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) {
|
||||
statusUpdated(new StatusEvent("Scanning... (" + event.getProgress() + "% complete" + (event.getRemainingAsString().isEmpty() ? ")" : ", " + event.getRemainingAsString() + " remaining)")));
|
||||
if(event.getProgress() > 0 && (statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING)) {
|
||||
statusBar.setProgress((double)event.getProgress() / 100);
|
||||
}
|
||||
} else if(event.isCompleted()) {
|
||||
serverToggle.setDisable(false);
|
||||
if(statusBar.getText().startsWith("Scanning...")) {
|
||||
statusBar.setText("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void cormorantPruneStatus(CormorantPruneStatusEvent event) {
|
||||
if(event.legacyWalletExists()) {
|
||||
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("Error importing Bitcoin Core descriptor wallet",
|
||||
"The connected node is pruned at " + event.getPruneDateAsString() + ", but the wallet birthday for " + event.getWallet().getFullDisplayName() + " is set to " + event.getScanDateAsString() + ".\n\n" +
|
||||
"Do you want to try using the existing legacy Bitcoin Core wallet?", ButtonType.YES, ButtonType.NO);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
Config.get().setUseLegacyCoreWallet(true);
|
||||
onlineProperty().set(false);
|
||||
Platform.runLater(() -> onlineProperty().set(true));
|
||||
}
|
||||
} else {
|
||||
AppServices.showErrorDialog("Error importing Bitcoin Core descriptor wallet",
|
||||
"The connected node is pruned at " + event.getPruneDateAsString() + ", but the wallet birthday for " + event.getWallet().getFullDisplayName() + " is set to " + event.getScanDateAsString() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtBootStatus(BwtBootStatusEvent event) {
|
||||
serverToggle.setDisable(true);
|
||||
@@ -2728,4 +2875,17 @@ public class AppController implements Initializable {
|
||||
lockAllWallets.setDisable(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletConfigChanged(WalletConfigChangedEvent event) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
if(tab.getUserData() instanceof WalletTabData walletTabData) {
|
||||
if(walletTabData.getWallet() == event.getWallet()) {
|
||||
Label tabLabel = (Label)tab.getGraphic();
|
||||
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
|
||||
walletIcon.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,26 +284,25 @@ public class AppServices {
|
||||
});
|
||||
connectionService.setOnFailed(failEvent -> {
|
||||
//Close connection here to create a new transport next time we try
|
||||
connectionService.resetConnection();
|
||||
connectionService.closeConnection();
|
||||
|
||||
if(failEvent.getSource().getException() instanceof ServerConfigException) {
|
||||
connectionService.setRestartOnFailure(false);
|
||||
}
|
||||
|
||||
if(failEvent.getSource().getException() instanceof TlsServerException && failEvent.getSource().getException().getCause() != null) {
|
||||
TlsServerException tlsServerException = (TlsServerException)failEvent.getSource().getException();
|
||||
if(failEvent.getSource().getException() instanceof TlsServerException tlsServerException && failEvent.getSource().getException().getCause() != null) {
|
||||
connectionService.setRestartOnFailure(false);
|
||||
if(tlsServerException.getCause().getMessage().contains("PKIX path building failed")) {
|
||||
File crtFile = Config.get().getElectrumServerCert();
|
||||
if(crtFile != null && Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
|
||||
AppServices.showErrorDialog("SSL Handshake Failed", "The configured server certificate at " + crtFile.getAbsolutePath() + " did not match the certificate provided by the server at " + tlsServerException.getServer().getHost() + "." +
|
||||
"\n\nThis may indicate a man-in-the-middle attack!" +
|
||||
"\n\nThis may be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." +
|
||||
"\n\nChange the configured server certificate if you would like to proceed.");
|
||||
} else {
|
||||
crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
|
||||
if(crtFile != null) {
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
|
||||
"\n\nThis may indicate a man-in-the-middle attack!" +
|
||||
"\n\nThis may be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." +
|
||||
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
|
||||
if(crtFile.delete()) {
|
||||
@@ -315,10 +314,19 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if(tlsServerException.getCause().getCause() instanceof UnknownCertificateExpiredException expiredException) {
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " has expired. "
|
||||
+ tlsServerException.getMessage() + "." +
|
||||
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
|
||||
Storage.saveCertificate(tlsServerException.getServer().getHost(), expiredException.getCertificate());
|
||||
Platform.runLater(() -> restartService(connectionService));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(failEvent.getSource().getException() instanceof ProxyServerException && Config.get().isUseProxy() && Config.get().requiresTor()) {
|
||||
if(failEvent.getSource().getException() instanceof ProxyServerException && Config.get().isUseProxy() && Config.get().isAutoSwitchProxy() && Config.get().requiresTor()) {
|
||||
Config.get().setUseProxy(false);
|
||||
Platform.runLater(() -> restartService(torService));
|
||||
return;
|
||||
@@ -1104,7 +1112,7 @@ public class AppServices {
|
||||
Wallet wallet = walletTabData.getWallet();
|
||||
Storage storage = walletTabData.getStorage();
|
||||
|
||||
if(Interface.get() == Interface.DESKTOP && (!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB))) {
|
||||
if(Interface.get() == Interface.DESKTOP && (!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB) || CardApi.isReaderAvailable())) {
|
||||
usbWallet = true;
|
||||
|
||||
if(deviceEnumerateService == null) {
|
||||
@@ -1139,10 +1147,12 @@ public class AppServices {
|
||||
@Subscribe
|
||||
public void requestDisconnect(RequestDisconnectEvent event) {
|
||||
onlineProperty.set(false);
|
||||
//Ensure services don't try to reconnect
|
||||
connectionService.cancel();
|
||||
ratesService.cancel();
|
||||
versionCheckService.cancel();
|
||||
//Ensure services don't try to reconnect later
|
||||
Platform.runLater(() -> {
|
||||
connectionService.cancel();
|
||||
ratesService.cancel();
|
||||
versionCheckService.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.control.WalletIcon;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
@@ -18,6 +19,7 @@ import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -43,6 +45,7 @@ public class SparrowDesktop extends Application {
|
||||
GlyphFontRegistry.register(new FontAwesome5());
|
||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
||||
URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null);
|
||||
|
||||
AppServices.initialize(this);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.*;
|
||||
public class SparrowWallet {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.7.0";
|
||||
public static final String APP_VERSION = "1.7.2";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
||||
@@ -8,15 +8,20 @@ import java.util.Locale;
|
||||
|
||||
public enum UnitFormat {
|
||||
DOT {
|
||||
private final DecimalFormat btcFormat = new DecimalFormat("0", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat btcFormat = new DecimalFormat("0", getDecimalFormatSymbols());
|
||||
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
|
||||
|
||||
public DecimalFormat getBtcFormat() {
|
||||
btcFormat.setMaximumFractionDigits(8);
|
||||
return btcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getSatsFormat() {
|
||||
return satsFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getTableBtcFormat() {
|
||||
return tableBtcFormat;
|
||||
}
|
||||
@@ -25,20 +30,28 @@ public enum UnitFormat {
|
||||
return currencyFormat;
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return Locale.ENGLISH;
|
||||
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
|
||||
symbols.setDecimalSeparator('.');
|
||||
symbols.setGroupingSeparator(',');
|
||||
return symbols;
|
||||
}
|
||||
},
|
||||
COMMA {
|
||||
private final DecimalFormat btcFormat = new DecimalFormat("0", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat btcFormat = new DecimalFormat("0", getDecimalFormatSymbols());
|
||||
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
|
||||
|
||||
public DecimalFormat getBtcFormat() {
|
||||
btcFormat.setMaximumFractionDigits(8);
|
||||
return btcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getSatsFormat() {
|
||||
return satsFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getTableBtcFormat() {
|
||||
return tableBtcFormat;
|
||||
}
|
||||
@@ -47,15 +60,20 @@ public enum UnitFormat {
|
||||
return currencyFormat;
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return Locale.GERMAN;
|
||||
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
|
||||
symbols.setDecimalSeparator(',');
|
||||
symbols.setGroupingSeparator('.');
|
||||
return symbols;
|
||||
}
|
||||
};
|
||||
|
||||
public abstract Locale getLocale();
|
||||
public abstract DecimalFormatSymbols getDecimalFormatSymbols();
|
||||
|
||||
public abstract DecimalFormat getBtcFormat();
|
||||
|
||||
public abstract DecimalFormat getSatsFormat();
|
||||
|
||||
public abstract DecimalFormat getTableBtcFormat();
|
||||
|
||||
public abstract DecimalFormat getCurrencyFormat();
|
||||
@@ -64,18 +82,18 @@ public enum UnitFormat {
|
||||
return getBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
}
|
||||
|
||||
public String tableFormatBtcValue(Long amount) {
|
||||
return getTableBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
}
|
||||
|
||||
public String formatSatsValue(Long amount) {
|
||||
return String.format(getLocale(), "%,d", amount);
|
||||
return getSatsFormat().format(amount);
|
||||
}
|
||||
|
||||
public String formatCurrencyValue(double amount) {
|
||||
return getCurrencyFormat().format(amount);
|
||||
}
|
||||
|
||||
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||
return DecimalFormatSymbols.getInstance(getLocale());
|
||||
}
|
||||
|
||||
public String getGroupingSeparator() {
|
||||
return Character.toString(getDecimalFormatSymbols().getGroupingSeparator());
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
if(utxoEntry != null) {
|
||||
Address address = addressStatus.getAddress();
|
||||
setText(address.toString());
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode()), false));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreCardImport;
|
||||
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
public class CardImportPane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardImportPane.class);
|
||||
|
||||
private final KeystoreCardImport importer;
|
||||
private final List<ChildNumber> derivation;
|
||||
protected Button importButton;
|
||||
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
||||
|
||||
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) {
|
||||
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
this.importer = importer;
|
||||
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
importButton = new Button("Import");
|
||||
Glyph tapGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI);
|
||||
tapGlyph.setFontSize(12);
|
||||
importButton.setGraphic(tapGlyph);
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importCard();
|
||||
});
|
||||
return importButton;
|
||||
}
|
||||
|
||||
private void importCard() {
|
||||
if(pin.get().length() < 6) {
|
||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||
setContent(getPinEntry());
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
importButton.setDisable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
StringProperty messageProperty = new SimpleStringProperty();
|
||||
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setDescription(newValue));
|
||||
});
|
||||
|
||||
try {
|
||||
if(!importer.isInitialized()) {
|
||||
setDescription("Card not initialized");
|
||||
setContent(getInitializationPanel(messageProperty));
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
return;
|
||||
}
|
||||
} catch(CardException e) {
|
||||
setError("Card Error", e.getMessage());
|
||||
importButton.setDisable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty);
|
||||
cardImportService.setOnSucceeded(event -> {
|
||||
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
|
||||
});
|
||||
cardImportService.setOnFailed(event -> {
|
||||
Throwable rootCause = Throwables.getRootCause(event.getSource().getException());
|
||||
if(rootCause instanceof CardAuthorizationException) {
|
||||
setError(rootCause.getMessage(), null);
|
||||
setContent(getPinEntry());
|
||||
} else {
|
||||
log.error("Error importing keystore from card", event.getSource().getException());
|
||||
setError("Import Error", rootCause.getMessage());
|
||||
}
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
cardImportService.start();
|
||||
}
|
||||
|
||||
private Node getInitializationPanel(StringProperty messageProperty) {
|
||||
VBox initTypeBox = new VBox(5);
|
||||
RadioButton automatic = new RadioButton("Automatic (Recommended)");
|
||||
RadioButton advanced = new RadioButton("Advanced");
|
||||
TextField entropy = new TextField();
|
||||
entropy.setPromptText("Enter input for user entropy");
|
||||
entropy.setDisable(true);
|
||||
|
||||
ToggleGroup toggleGroup = new ToggleGroup();
|
||||
automatic.setToggleGroup(toggleGroup);
|
||||
advanced.setToggleGroup(toggleGroup);
|
||||
automatic.setSelected(true);
|
||||
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
entropy.setDisable(newValue == automatic);
|
||||
});
|
||||
|
||||
initTypeBox.getChildren().addAll(automatic, advanced, entropy);
|
||||
|
||||
Button initializeButton = new Button("Initialize");
|
||||
initializeButton.setDefaultButton(true);
|
||||
initializeButton.setOnAction(event -> {
|
||||
initializeButton.setDisable(true);
|
||||
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
|
||||
CardInitializationService cardInitializationService = new CardInitializationService(importer, pin.get(), chainCode, messageProperty);
|
||||
cardInitializationService.setOnSucceeded(successEvent -> {
|
||||
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
|
||||
setDescription("Leave card on reader");
|
||||
setExpanded(false);
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
cardInitializationService.setOnFailed(failEvent -> {
|
||||
Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
|
||||
if(rootCause instanceof CardAuthorizationException) {
|
||||
setError(rootCause.getMessage(), null);
|
||||
setContent(getPinEntry());
|
||||
importButton.setDisable(false);
|
||||
} else {
|
||||
log.error("Error initializing card", failEvent.getSource().getException());
|
||||
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
|
||||
initializeButton.setDisable(false);
|
||||
}
|
||||
});
|
||||
cardInitializationService.start();
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox(20);
|
||||
contentBox.getChildren().addAll(initTypeBox, initializeButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
HBox.setHgrow(initTypeBox, Priority.ALWAYS);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private Node getPinEntry() {
|
||||
VBox vBox = new VBox();
|
||||
|
||||
CustomPasswordField pinField = new ViewPasswordField();
|
||||
pinField.setPromptText("PIN Code");
|
||||
importButton.setDefaultButton(true);
|
||||
pin.bind(pinField.textProperty());
|
||||
HBox.setHgrow(pinField, Priority.ALWAYS);
|
||||
Platform.runLater(pinField::requestFocus);
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().add(pinField);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(50);
|
||||
|
||||
vBox.getChildren().add(contentBox);
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
public static class CardInitializationService extends Service<Void> {
|
||||
private final KeystoreCardImport cardImport;
|
||||
private final String pin;
|
||||
private final byte[] chainCode;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public CardInitializationService(KeystoreCardImport cardImport, String pin, byte[] chainCode, StringProperty messageProperty) {
|
||||
this.cardImport = cardImport;
|
||||
this.pin = pin;
|
||||
this.chainCode = chainCode;
|
||||
this.messageProperty = messageProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
cardImport.initialize(pin, chainCode, messageProperty);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class CardImportService extends Service<Keystore> {
|
||||
private final KeystoreCardImport cardImport;
|
||||
private final String pin;
|
||||
private final List<ChildNumber> derivation;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public CardImportService(KeystoreCardImport cardImport, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
this.cardImport = cardImport;
|
||||
this.pin = pin;
|
||||
this.derivation = derivation;
|
||||
this.messageProperty = messageProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Keystore> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Keystore call() throws Exception {
|
||||
return cardImport.getKeystore(pin, derivation, messageProperty);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.scene.control.*;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
public class CardPinDialog extends Dialog<CardPinDialog.CardPinChange> {
|
||||
private final CustomPasswordField existingPin;
|
||||
private final CustomPasswordField newPin;
|
||||
private final CustomPasswordField newPinConfirm;
|
||||
private final CheckBox backupFirst;
|
||||
private final ButtonType okButtonType;
|
||||
|
||||
public CardPinDialog() {
|
||||
this.existingPin = new ViewPasswordField();
|
||||
this.newPin = new ViewPasswordField();
|
||||
this.newPinConfirm = new ViewPasswordField();
|
||||
this.backupFirst = new CheckBox();
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("Change Card PIN");
|
||||
dialogPane.setHeaderText("Enter the current PIN, and then the new PIN twice. PIN must be between 6 and 32 digits.");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(260);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Glyph lock = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
|
||||
lock.setFontSize(50);
|
||||
dialogPane.setGraphic(lock);
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
fieldset.setSpacing(10);
|
||||
|
||||
Field currentField = new Field();
|
||||
currentField.setText("Current PIN:");
|
||||
currentField.getInputs().add(existingPin);
|
||||
|
||||
Field newField = new Field();
|
||||
newField.setText("New PIN:");
|
||||
newField.getInputs().add(newPin);
|
||||
|
||||
Field confirmField = new Field();
|
||||
confirmField.setText("Confirm new PIN:");
|
||||
confirmField.getInputs().add(newPinConfirm);
|
||||
|
||||
Field backupField = new Field();
|
||||
backupField.setText("Backup First:");
|
||||
backupField.getInputs().add(backupFirst);
|
||||
|
||||
fieldset.getChildren().addAll(currentField, newField, confirmField, backupField);
|
||||
form.getChildren().add(fieldset);
|
||||
dialogPane.setContent(form);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater( () -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(existingPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", existingPin.getText().length() < 6 || existingPin.getText().length() > 32));
|
||||
validationSupport.registerValidator(newPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", newPin.getText().length() < 6 || newPin.getText().length() > 32));
|
||||
validationSupport.registerValidator(newPinConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "PIN confirmation does not match", !newPinConfirm.getText().equals(newPin.getText())));
|
||||
});
|
||||
|
||||
okButtonType = new javafx.scene.control.ButtonType("Change", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||
okButton.setPrefWidth(130);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> existingPin.getText().length() < 6 || existingPin.getText().length() > 32
|
||||
|| newPin.getText().length() < 6 || newPin.getText().length() > 32
|
||||
|| !newPin.getText().equals(newPinConfirm.getText()),
|
||||
existingPin.textProperty(), newPin.textProperty(), newPinConfirm.textProperty());
|
||||
okButton.disableProperty().bind(isInvalid);
|
||||
|
||||
Platform.runLater(existingPin::requestFocus);
|
||||
setResultConverter(dialogButton -> dialogButton == okButtonType ? new CardPinChange(existingPin.getText(), newPin.getText(), backupFirst.isSelected()) : null);
|
||||
}
|
||||
|
||||
public record CardPinChange(String currentPin, String newPin, boolean backupFirst) { }
|
||||
}
|
||||
@@ -2,11 +2,14 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
@@ -16,12 +19,14 @@ import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
private final Tooltip tooltip;
|
||||
class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsListener {
|
||||
private final CoinTooltip tooltip;
|
||||
|
||||
private IntegerProperty confirmationsProperty;
|
||||
|
||||
public CoinCell() {
|
||||
super();
|
||||
tooltip = new Tooltip();
|
||||
tooltip = new CoinTooltip();
|
||||
tooltip.setShowDelay(Duration.millis(500));
|
||||
getStyleClass().add("coin-cell");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
@@ -50,21 +55,16 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setText(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
|
||||
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
|
||||
setText(btcValue);
|
||||
} else {
|
||||
tooltip.setText(btcValue + " " + BitcoinUnit.BTC.getLabel());
|
||||
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
|
||||
setText(satsValue);
|
||||
}
|
||||
setTooltip(tooltip);
|
||||
String tooltipValue = tooltip.getText();
|
||||
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
tooltip.setText(tooltipValue + " (" + transactionEntry.getConfirmationsDescription() + ")");
|
||||
|
||||
transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> {
|
||||
tooltip.setText(tooltipValue + " (" + transactionEntry.getConfirmationsDescription() + ")");
|
||||
});
|
||||
tooltip.showConfirmations(transactionEntry.confirmationsProperty());
|
||||
|
||||
if(transactionEntry.isConfirming()) {
|
||||
ConfirmationProgressIndicator arc = new ConfirmationProgressIndicator(transactionEntry.getConfirmations());
|
||||
@@ -94,4 +94,65 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegerProperty getConfirmationsProperty() {
|
||||
if(confirmationsProperty == null) {
|
||||
confirmationsProperty = new SimpleIntegerProperty();
|
||||
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
|
||||
getStyleClass().remove("confirming");
|
||||
confirmationsProperty.unbind();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return confirmationsProperty;
|
||||
}
|
||||
|
||||
private static final class CoinTooltip extends Tooltip {
|
||||
private final IntegerProperty confirmationsProperty = new SimpleIntegerProperty();
|
||||
private boolean showConfirmations;
|
||||
private String value;
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
setTooltipText();
|
||||
}
|
||||
|
||||
public void showConfirmations(IntegerProperty txEntryConfirmationsProperty) {
|
||||
showConfirmations = true;
|
||||
|
||||
int confirmations = txEntryConfirmationsProperty.get();
|
||||
if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||
confirmationsProperty.bind(txEntryConfirmationsProperty);
|
||||
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
|
||||
setTooltipText();
|
||||
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||
confirmationsProperty.unbind();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
confirmationsProperty.unbind();
|
||||
confirmationsProperty.set(confirmations);
|
||||
}
|
||||
|
||||
setTooltipText();
|
||||
}
|
||||
|
||||
private void setTooltipText() {
|
||||
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
|
||||
}
|
||||
|
||||
public String getConfirmationsDescription() {
|
||||
int confirmations = confirmationsProperty.get();
|
||||
if(confirmations == 0) {
|
||||
return "Unconfirmed in mempool";
|
||||
} else if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s");
|
||||
} else {
|
||||
return BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM + "+ confirmations";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||
|
||||
public CoinFilter(UnitFormat unitFormat) {
|
||||
this.unitFormat = unitFormat == null ? UnitFormat.DOT : unitFormat;
|
||||
this.coinFormat = new DecimalFormat("###,###.########", DecimalFormatSymbols.getInstance(unitFormat.getLocale()));
|
||||
this.coinFormat = new DecimalFormat("###,###.########", unitFormat.getDecimalFormatSymbols());
|
||||
this.coinValidation = Pattern.compile("[\\d" + Pattern.quote(unitFormat.getGroupingSeparator()) + "]*(" + Pattern.quote(unitFormat.getDecimalSeparator()) + "\\d{0,8})?");
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
Hyperlink hyperlink = new Hyperlink();
|
||||
hyperlink.setTranslateY(30);
|
||||
hyperlink.setOnAction(event -> {
|
||||
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
|
||||
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate(), false);
|
||||
Optional<Date> optDate = dlg.showAndWait();
|
||||
if(optDate.isPresent()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
|
||||
public interface ConfirmationsListener {
|
||||
IntegerProperty getConfirmationsProperty();
|
||||
}
|
||||
@@ -68,7 +68,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
|
||||
stackPane.getChildren().addAll(anchorPane, scanBox);
|
||||
|
||||
List<Device> devices = AppServices.getDevices();
|
||||
List<Device> devices = getDevices();
|
||||
if(devices == null || devices.isEmpty()) {
|
||||
scanButton.setDefaultButton(true);
|
||||
scanBox.setVisible(true);
|
||||
@@ -96,6 +96,10 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
|
||||
}
|
||||
|
||||
protected List<Device> getDevices() {
|
||||
return AppServices.getDevices();
|
||||
}
|
||||
|
||||
private void scan() {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
|
||||
+2
-2
@@ -9,11 +9,11 @@ import com.sparrowwallet.sparrow.io.Device;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DeviceAddressDialog extends DeviceDialog<String> {
|
||||
public class DeviceDisplayAddressDialog extends DeviceDialog<String> {
|
||||
private final Wallet wallet;
|
||||
private final OutputDescriptor outputDescriptor;
|
||||
|
||||
public DeviceAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
|
||||
public DeviceDisplayAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
|
||||
super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
|
||||
this.wallet = wallet;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.DeviceAddressEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DeviceGetAddressDialog extends DeviceDialog<Address> {
|
||||
public DeviceGetAddressDialog(List<String> operationFingerprints) {
|
||||
super(operationFingerprints);
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(DevicePane.DeviceOperation.GET_ADDRESS, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void deviceAddress(DeviceAddressEvent event) {
|
||||
setResult(event.getAddress());
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,33 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
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.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.CardApi;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.WorkerStateEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
@@ -34,8 +42,10 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class DevicePane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
|
||||
@@ -58,8 +68,12 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
private Button displayAddressButton;
|
||||
private Button signMessageButton;
|
||||
private Button discoverKeystoresButton;
|
||||
private ButtonBase getPrivateKeyButton;
|
||||
private Button getAddressButton;
|
||||
|
||||
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
||||
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
||||
private final StringProperty messageProperty = new SimpleStringProperty("");
|
||||
|
||||
private boolean defaultDevice;
|
||||
|
||||
@@ -83,13 +97,17 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
initialise(device);
|
||||
|
||||
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setDescription(newValue));
|
||||
});
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
|
||||
}
|
||||
|
||||
public DevicePane(PSBT psbt, Device device, boolean defaultDevice) {
|
||||
public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.SIGN;
|
||||
this.wallet = null;
|
||||
this.wallet = wallet;
|
||||
this.psbt = psbt;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
@@ -106,6 +124,10 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
initialise(device);
|
||||
|
||||
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setDescription(newValue));
|
||||
});
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
|
||||
}
|
||||
|
||||
@@ -152,6 +174,10 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
initialise(device);
|
||||
|
||||
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setDescription(newValue));
|
||||
});
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
|
||||
}
|
||||
|
||||
@@ -178,6 +204,41 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
|
||||
}
|
||||
|
||||
public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = deviceOperation;
|
||||
this.wallet = null;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
this.availableAccounts = null;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
|
||||
ButtonBase button;
|
||||
if(deviceOperation == DeviceOperation.GET_PRIVATE_KEY) {
|
||||
createGetPrivateKeyButton();
|
||||
button = getPrivateKeyButton;
|
||||
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
|
||||
createGetAddressButton();
|
||||
button = getAddressButton;
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot construct device pane for operation " + deviceOperation);
|
||||
}
|
||||
|
||||
initialise(device);
|
||||
|
||||
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setDescription(newValue));
|
||||
});
|
||||
|
||||
buttonBox.getChildren().add(button);
|
||||
}
|
||||
|
||||
private void initialise(Device device) {
|
||||
if(device.isNeedsPinSent()) {
|
||||
unlockButton.setDefaultButton(defaultDevice);
|
||||
@@ -201,7 +262,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void setDefaultStatus() {
|
||||
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : "Unlocked");
|
||||
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : device.isCard() ? "Leave card on reader" : "Unlocked");
|
||||
}
|
||||
|
||||
private void createUnlockButton() {
|
||||
@@ -321,6 +382,60 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
discoverKeystoresButton.setVisible(false);
|
||||
}
|
||||
|
||||
private void createGetPrivateKeyButton() {
|
||||
int currentSlot = 0;
|
||||
boolean initialized = true;
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), null);
|
||||
currentSlot = cardApi.getCurrentSlot();
|
||||
initialized = cardApi.isInitialized();
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
getPrivateKeyButton = currentSlot > 0 ? new SplitMenuButton() : new Button();
|
||||
getPrivateKeyButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
getPrivateKeyButton.setText("Get Private Key");
|
||||
getPrivateKeyButton.setOnAction(event -> {
|
||||
getPrivateKeyButton.setDisable(true);
|
||||
getPrivateKey(null);
|
||||
});
|
||||
|
||||
if(getPrivateKeyButton instanceof SplitMenuButton getPrivateKeyMenuButton) {
|
||||
int[] previousSlots = IntStream.range(0, currentSlot).toArray();
|
||||
for(int previousSlot : previousSlots) {
|
||||
MenuItem previousSlotItem = new MenuItem("Slot #" + previousSlot);
|
||||
previousSlotItem.setOnAction(event -> {
|
||||
getPrivateKeyButton.setDisable(true);
|
||||
getPrivateKey(previousSlot);
|
||||
});
|
||||
getPrivateKeyMenuButton.getItems().add(previousSlotItem);
|
||||
}
|
||||
if(initialized) {
|
||||
int finalSlot = currentSlot;
|
||||
MenuItem currentSlotItem = new MenuItem("Current Slot");
|
||||
currentSlotItem.setOnAction(event -> {
|
||||
getPrivateKeyButton.setDisable(true);
|
||||
getPrivateKey(finalSlot);
|
||||
});
|
||||
getPrivateKeyMenuButton.getItems().add(currentSlotItem);
|
||||
}
|
||||
}
|
||||
getPrivateKeyButton.managedProperty().bind(getPrivateKeyButton.visibleProperty());
|
||||
getPrivateKeyButton.setVisible(false);
|
||||
}
|
||||
|
||||
private void createGetAddressButton() {
|
||||
getAddressButton = new Button("Get Address");
|
||||
getAddressButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
getAddressButton.setOnAction(event -> {
|
||||
getAddressButton.setDisable(true);
|
||||
getAddress();
|
||||
});
|
||||
getAddressButton.managedProperty().bind(getAddressButton.visibleProperty());
|
||||
getAddressButton.setVisible(false);
|
||||
}
|
||||
|
||||
private void unlock(Device device) {
|
||||
if(device.getModel().requiresPinPrompt()) {
|
||||
promptPin();
|
||||
@@ -554,7 +669,36 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void importKeystore(List<ChildNumber> derivation) {
|
||||
if(device.getFingerprint() == null) {
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||
if(!cardApi.isInitialized()) {
|
||||
if(pin.get().length() < 6) {
|
||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||
setContent(getCardPinEntry(importButton));
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
importButton.setDisable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setDescription("Card not initialized");
|
||||
setContent(getCardInitializationPanel(cardApi, importButton, DeviceOperation.IMPORT));
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
|
||||
handleCardOperation(importService, importButton, "Import", true, event -> {
|
||||
importKeystore(derivation, importService.getValue());
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Import Error: " + e.getMessage(), e);
|
||||
setError("Import Error", e.getMessage());
|
||||
importButton.setDisable(false);
|
||||
}
|
||||
} else if(device.getFingerprint() == null) {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
List<Device> devices = enumerateService.getValue();
|
||||
@@ -591,18 +735,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
|
||||
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
|
||||
wallet.setName(device.getModel().toDisplayString());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
importKeystore(derivation, keystore);
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve xpub", e.getMessage());
|
||||
}
|
||||
@@ -616,20 +749,74 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
getXpubService.start();
|
||||
}
|
||||
|
||||
private void importKeystore(List<ChildNumber> derivation, Keystore keystore) {
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
|
||||
wallet.setName(device.getModel().toDisplayString());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
}
|
||||
|
||||
private void sign() {
|
||||
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
|
||||
signPSBTService.setOnSucceeded(workerStateEvent -> {
|
||||
PSBT signedPsbt = signPSBTService.getValue();
|
||||
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||
Service<PSBT> signService = cardApi.getSignService(wallet, psbt, messageProperty);
|
||||
handleCardOperation(signService, signButton, "Signing", true, event -> {
|
||||
EventManager.get().post(new PSBTSignedEvent(psbt, signService.getValue()));
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Signing Error: " + e.getMessage(), e);
|
||||
setError("Signing Error", e.getMessage());
|
||||
signButton.setDisable(false);
|
||||
}
|
||||
} else {
|
||||
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
|
||||
signPSBTService.setOnSucceeded(workerStateEvent -> {
|
||||
PSBT signedPsbt = signPSBTService.getValue();
|
||||
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
|
||||
});
|
||||
signPSBTService.setOnFailed(workerStateEvent -> {
|
||||
setError("Signing Error", signPSBTService.getException().getMessage());
|
||||
log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException());
|
||||
signButton.setDisable(false);
|
||||
});
|
||||
setDescription("Signing...");
|
||||
showHideLink.setVisible(false);
|
||||
signPSBTService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, boolean pinRequired, EventHandler<WorkerStateEvent> successHandler) {
|
||||
if(pinRequired && pin.get().length() < 6) {
|
||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||
setContent(getCardPinEntry(operationButton));
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
operationButton.setDisable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
service.setOnSucceeded(successHandler);
|
||||
service.setOnFailed(event -> {
|
||||
Throwable rootCause = Throwables.getRootCause(event.getSource().getException());
|
||||
if(rootCause instanceof CardAuthorizationException) {
|
||||
setError(rootCause.getMessage(), null);
|
||||
setContent(getCardPinEntry(operationButton));
|
||||
} else {
|
||||
log.error(operationDescription + " Error: " + rootCause.getMessage(), event.getSource().getException());
|
||||
setError(operationDescription + " Error", rootCause.getMessage());
|
||||
}
|
||||
operationButton.setDisable(false);
|
||||
});
|
||||
signPSBTService.setOnFailed(workerStateEvent -> {
|
||||
setError("Signing Error", signPSBTService.getException().getMessage());
|
||||
log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException());
|
||||
signButton.setDisable(false);
|
||||
});
|
||||
setDescription("Signing...");
|
||||
showHideLink.setVisible(false);
|
||||
signPSBTService.start();
|
||||
service.start();
|
||||
}
|
||||
|
||||
private void displayAddress() {
|
||||
@@ -647,17 +834,32 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void signMessage() {
|
||||
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
|
||||
signMessageService.setOnSucceeded(successEvent -> {
|
||||
String signature = signMessageService.getValue();
|
||||
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||
});
|
||||
signMessageService.setOnFailed(failedEvent -> {
|
||||
setError("Could not sign message", signMessageService.getException().getMessage());
|
||||
signMessageButton.setDisable(false);
|
||||
});
|
||||
setDescription("Signing message...");
|
||||
signMessageService.start();
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty);
|
||||
handleCardOperation(signMessageService, signMessageButton, "Signing", true, event -> {
|
||||
String signature = signMessageService.getValue();
|
||||
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Signing Error: " + e.getMessage(), e);
|
||||
setError("Signing Error", e.getMessage());
|
||||
signButton.setDisable(false);
|
||||
}
|
||||
} else {
|
||||
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
|
||||
signMessageService.setOnSucceeded(successEvent -> {
|
||||
String signature = signMessageService.getValue();
|
||||
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||
});
|
||||
signMessageService.setOnFailed(failedEvent -> {
|
||||
setError("Could not sign message", signMessageService.getException().getMessage());
|
||||
signMessageButton.setDisable(false);
|
||||
});
|
||||
setDescription("Signing message...");
|
||||
signMessageService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void discoverKeystores() {
|
||||
@@ -717,6 +919,55 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
getXpubsService.start();
|
||||
}
|
||||
|
||||
private void getPrivateKey(Integer slot) {
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||
Service<ECKey> privateKeyService = cardApi.getPrivateKeyService(slot, messageProperty);
|
||||
handleCardOperation(privateKeyService, getPrivateKeyButton, "Private Key", true, event -> {
|
||||
EventManager.get().post(new DeviceGetPrivateKeyEvent(privateKeyService.getValue(), cardApi.getDefaultScriptType()));
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Private Key Error: " + e.getMessage(), e);
|
||||
setError("Private Key Error", e.getMessage());
|
||||
getPrivateKeyButton.setDisable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void getAddress() {
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||
if(!cardApi.isInitialized()) {
|
||||
if(pin.get().length() < 6) {
|
||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||
setContent(getCardPinEntry(getAddressButton));
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
getAddressButton.setDisable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setDescription("Card not initialized");
|
||||
setContent(getCardInitializationPanel(cardApi, getAddressButton, DeviceOperation.GET_ADDRESS));
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
Service<Address> addressService = cardApi.getAddressService(messageProperty);
|
||||
handleCardOperation(addressService, getAddressButton, "Address", false, event -> {
|
||||
EventManager.get().post(new DeviceAddressEvent(addressService.getValue()));
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Address Error: " + e.getMessage(), e);
|
||||
setError("Address Error", e.getMessage());
|
||||
getAddressButton.setDisable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showOperationButton() {
|
||||
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||
if(defaultDevice) {
|
||||
@@ -724,7 +975,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
importButton.setVisible(true);
|
||||
showHideLink.setText("Show derivation...");
|
||||
showHideLink.setVisible(true);
|
||||
showHideLink.setVisible(!device.isCard());
|
||||
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)) {
|
||||
@@ -743,6 +994,16 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
discoverKeystoresButton.setDefaultButton(defaultDevice);
|
||||
discoverKeystoresButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.GET_PRIVATE_KEY)) {
|
||||
if(defaultDevice) {
|
||||
getPrivateKeyButton.getStyleClass().add("default-button");
|
||||
}
|
||||
getPrivateKeyButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.GET_ADDRESS)) {
|
||||
getAddressButton.setDefaultButton(defaultDevice);
|
||||
getAddressButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -750,7 +1011,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||
derivationField.setDisable(keyDerivation != null);
|
||||
derivationField.setDisable(device.isCard() || keyDerivation != null);
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
@@ -785,11 +1046,92 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private Node getCardInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
|
||||
VBox initTypeBox = new VBox(5);
|
||||
RadioButton automatic = new RadioButton("Automatic (Recommended)");
|
||||
RadioButton advanced = new RadioButton("Advanced");
|
||||
TextField entropy = new TextField();
|
||||
entropy.setPromptText("Enter input for user entropy");
|
||||
entropy.setDisable(true);
|
||||
|
||||
ToggleGroup toggleGroup = new ToggleGroup();
|
||||
automatic.setToggleGroup(toggleGroup);
|
||||
advanced.setToggleGroup(toggleGroup);
|
||||
automatic.setSelected(true);
|
||||
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
entropy.setDisable(newValue == automatic);
|
||||
});
|
||||
|
||||
initTypeBox.getChildren().addAll(automatic, advanced, entropy);
|
||||
|
||||
Button initializeButton = new Button("Initialize");
|
||||
initializeButton.setDefaultButton(true);
|
||||
initializeButton.setOnAction(event -> {
|
||||
initializeButton.setDisable(true);
|
||||
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
|
||||
Service<Void> cardInitializationService = cardApi.getInitializationService(chainCode, messageProperty);
|
||||
cardInitializationService.setOnSucceeded(successEvent -> {
|
||||
if(deviceOperation == DeviceOperation.IMPORT) {
|
||||
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
|
||||
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
|
||||
AppServices.showSuccessDialog("Card Reinitialized", "The card was successfully reinitialized.\n\nYou can now retrieve the new deposit address.");
|
||||
}
|
||||
operationButton.setDisable(false);
|
||||
setDefaultStatus();
|
||||
setExpanded(false);
|
||||
});
|
||||
cardInitializationService.setOnFailed(failEvent -> {
|
||||
Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
|
||||
if(rootCause instanceof CardAuthorizationException) {
|
||||
setError(rootCause.getMessage(), null);
|
||||
setContent(getCardPinEntry(operationButton));
|
||||
operationButton.setDisable(false);
|
||||
} else {
|
||||
log.error("Error initializing card", failEvent.getSource().getException());
|
||||
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
|
||||
initializeButton.setDisable(false);
|
||||
}
|
||||
});
|
||||
cardInitializationService.start();
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox(20);
|
||||
contentBox.getChildren().addAll(initTypeBox, initializeButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
HBox.setHgrow(initTypeBox, Priority.ALWAYS);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private Node getCardPinEntry(ButtonBase operationButton) {
|
||||
VBox vBox = new VBox();
|
||||
|
||||
CustomPasswordField pinField = new ViewPasswordField();
|
||||
pinField.setPromptText("PIN Code");
|
||||
if(operationButton instanceof Button defaultButton) {
|
||||
defaultButton.setDefaultButton(true);
|
||||
}
|
||||
pin.bind(pinField.textProperty());
|
||||
HBox.setHgrow(pinField, Priority.ALWAYS);
|
||||
Platform.runLater(pinField::requestFocus);
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().add(pinField);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(50);
|
||||
|
||||
vBox.getChildren().add(contentBox);
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
public Device getDevice() {
|
||||
return device;
|
||||
}
|
||||
|
||||
public enum DeviceOperation {
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES;
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, GET_PRIVATE_KEY, GET_ADDRESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,24 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DeviceSignDialog extends DeviceDialog<PSBT> {
|
||||
private static final Logger log = LoggerFactory.getLogger(DeviceSignDialog.class);
|
||||
|
||||
private final Wallet wallet;
|
||||
private final PSBT psbt;
|
||||
|
||||
public DeviceSignDialog(List<String> operationFingerprints, PSBT psbt) {
|
||||
public DeviceSignDialog(Wallet wallet, List<String> operationFingerprints, PSBT psbt) {
|
||||
super(operationFingerprints);
|
||||
this.wallet = wallet;
|
||||
this.psbt = psbt;
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
@@ -23,7 +30,7 @@ public class DeviceSignDialog extends DeviceDialog<PSBT> {
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(psbt, device, defaultDevice);
|
||||
return new DevicePane(wallet, psbt, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.DeviceGetPrivateKeyEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.DevicePrivateKey> {
|
||||
public DeviceUnsealDialog(List<String> operationFingerprints) {
|
||||
super(operationFingerprints);
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(DevicePane.DeviceOperation.GET_PRIVATE_KEY, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void deviceGetPrivateKey(DeviceGetPrivateKeyEvent event) {
|
||||
setResult(new DevicePrivateKey(event.getPrivateKey(), event.getScriptType()));
|
||||
}
|
||||
|
||||
public record DevicePrivateKey(ECKey privateKey, ScriptType scriptType) {}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
@@ -30,7 +32,7 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
public class EntryCell extends TreeTableCell<Entry, Entry> implements ConfirmationsListener {
|
||||
private static final Logger log = LoggerFactory.getLogger(EntryCell.class);
|
||||
|
||||
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
@@ -38,6 +40,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
private static EntryCell lastCell;
|
||||
|
||||
private IntegerProperty confirmationsProperty;
|
||||
|
||||
public EntryCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
@@ -120,7 +124,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||
Address address = nodeEntry.getAddress();
|
||||
setText(address.toString());
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(nodeEntry.getNode().toString());
|
||||
@@ -189,6 +193,21 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegerProperty getConfirmationsProperty() {
|
||||
if(confirmationsProperty == null) {
|
||||
confirmationsProperty = new SimpleIntegerProperty();
|
||||
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
|
||||
getStyleClass().remove("confirming");
|
||||
confirmationsProperty.unbind();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return confirmationsProperty;
|
||||
}
|
||||
|
||||
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
||||
@@ -334,7 +353,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
private static boolean canSignMessage(WalletNode walletNode) {
|
||||
Wallet wallet = walletNode.getWallet();
|
||||
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
|
||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB) &&
|
||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB || wallet.getKeystores().get(0).getWalletModel().isCard()) &&
|
||||
(!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
@@ -568,7 +587,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
|
||||
public static class AddressContextMenu extends ContextMenu {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry, boolean addUtxoItems) {
|
||||
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
@@ -591,7 +610,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
getItems().add(signVerifyMessage);
|
||||
}
|
||||
|
||||
if(nodeEntry != null && !nodeEntry.getNode().getUnspentTransactionOutputs().isEmpty()) {
|
||||
if(addUtxoItems && nodeEntry != null && !nodeEntry.getNode().getUnspentTransactionOutputs().isEmpty()) {
|
||||
List<BlockTransactionHashIndex> utxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().collect(Collectors.toList());
|
||||
MenuItem spendUtxos = new MenuItem("Spend UTXOs");
|
||||
spendUtxos.setGraphic(getSendGlyph());
|
||||
@@ -635,14 +654,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
MenuItem copyHex = new MenuItem("Copy Script Output Bytes");
|
||||
copyHex.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(Utils.bytesToHex(address.getOutputScriptData()));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
MenuItem copyOutputDescriptor = new MenuItem("Copy Output Descriptor");
|
||||
copyOutputDescriptor.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -651,7 +662,19 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
|
||||
getItems().addAll(copyAddress, copyOutputDescriptor);
|
||||
|
||||
if(nodeEntry != null) {
|
||||
MenuItem copyHex = new MenuItem("Copy Script Output Bytes");
|
||||
copyHex.setOnAction(AE -> {
|
||||
hide();
|
||||
Script outputScript = nodeEntry.getWallet().getOutputScript(nodeEntry.getNode());
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(Utils.bytesToHex(outputScript.getProgram()));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyHex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,13 +745,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
if(entry instanceof TransactionEntry) {
|
||||
cell.getStyleClass().add("transaction-row");
|
||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||
if(transactionEntry.isConfirming()) {
|
||||
cell.getStyleClass().add("confirming");
|
||||
transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(!transactionEntry.isConfirming()) {
|
||||
cell.getStyleClass().remove("confirming");
|
||||
}
|
||||
});
|
||||
|
||||
if(cell instanceof ConfirmationsListener confirmationsListener) {
|
||||
if(transactionEntry.isConfirming()) {
|
||||
cell.getStyleClass().add("confirming");
|
||||
confirmationsListener.getConfirmationsProperty().bind(transactionEntry.confirmationsProperty());
|
||||
} else {
|
||||
confirmationsListener.getConfirmationsProperty().unbind();
|
||||
}
|
||||
}
|
||||
} else if(entry instanceof NodeEntry) {
|
||||
cell.getStyleClass().add("node-row");
|
||||
|
||||
@@ -34,17 +34,4 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
}
|
||||
|
||||
private static int getAccount(Wallet wallet, KeyDerivation requiredDerivation) {
|
||||
if(wallet == null || requiredDerivation == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int account = wallet.getScriptType().getAccount(requiredDerivation.getDerivationPath());
|
||||
if(account < 0) {
|
||||
account = 0;
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,14 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
private final Wallet wallet;
|
||||
private final WalletExport exporter;
|
||||
private final boolean scannable;
|
||||
private final boolean file;
|
||||
|
||||
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
|
||||
super(exporter.getName(), "Wallet file export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||
this.wallet = wallet;
|
||||
this.exporter = exporter;
|
||||
this.scannable = exporter.isWalletExportScannable();
|
||||
this.file = exporter.isWalletExportFile();
|
||||
|
||||
buttonBox.getChildren().clear();
|
||||
buttonBox.getChildren().add(createButton());
|
||||
@@ -41,7 +43,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
if(scannable) {
|
||||
if(scannable && file) {
|
||||
ToggleButton showButton = new ToggleButton("Show...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
@@ -61,6 +63,15 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
SegmentedButton segmentedButton = new SegmentedButton();
|
||||
segmentedButton.getButtons().addAll(showButton, fileButton);
|
||||
return segmentedButton;
|
||||
} else if(scannable) {
|
||||
Button showButton = new Button("Show...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
showButton.setGraphic(cameraGlyph);
|
||||
showButton.setOnAction(event -> {
|
||||
exportQR();
|
||||
});
|
||||
return showButton;
|
||||
} else {
|
||||
Button exportButton = new Button("Export File...");
|
||||
exportButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -137,7 +148,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
QRDisplayDialog qrDisplayDialog;
|
||||
if(exporter instanceof CoboVaultMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig) {
|
||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
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.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
public class KeystorePassphraseDialog extends Dialog<String> {
|
||||
private final CustomPasswordField passphrase;
|
||||
private final ObjectProperty<byte[]> masterFingerprint = new SimpleObjectProperty<>();
|
||||
|
||||
public KeystorePassphraseDialog(Keystore keystore) {
|
||||
this(null, keystore);
|
||||
@@ -45,10 +48,38 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||
content.setPrefHeight(50);
|
||||
content.getChildren().add(passphrase);
|
||||
|
||||
passphrase.textProperty().addListener((observable, oldValue, passphrase) -> {
|
||||
masterFingerprint.set(getMasterFingerprint(keystore, passphrase));
|
||||
});
|
||||
|
||||
HBox fingerprintBox = new HBox(10);
|
||||
fingerprintBox.setAlignment(Pos.CENTER_LEFT);
|
||||
Label fingerprintLabel = new Label("Master fingerprint:");
|
||||
TextField fingerprintHex = new TextField();
|
||||
fingerprintHex.setDisable(true);
|
||||
fingerprintHex.setMaxWidth(80);
|
||||
fingerprintHex.getStyleClass().addAll("fixed-width");
|
||||
fingerprintHex.setStyle("-fx-opacity: 0.6");
|
||||
masterFingerprint.addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null) {
|
||||
fingerprintHex.setText(Utils.bytesToHex(newValue));
|
||||
}
|
||||
});
|
||||
LifeHashIcon lifeHashIcon = new LifeHashIcon();
|
||||
lifeHashIcon.dataProperty().bind(masterFingerprint);
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setHelpText("All passphrases create valid wallets." +
|
||||
"\nThe master fingerprint identifies the keystore and changes as the passphrase changes." +
|
||||
"\n" + (confirm ? "Take a moment to identify it before proceeding." : "Make sure you recognise it before proceeding."));
|
||||
fingerprintBox.getChildren().addAll(fingerprintLabel, fingerprintHex, lifeHashIcon, helpLabel);
|
||||
content.getChildren().add(fingerprintBox);
|
||||
|
||||
masterFingerprint.set(getMasterFingerprint(keystore, ""));
|
||||
|
||||
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);
|
||||
Label warnLabel = new Label((confirm ? "Note" : "Check") + " the master fingerprint before proceeding!", warnGlyph);
|
||||
warnLabel.setGraphicTextGap(5);
|
||||
content.getChildren().add(warnLabel);
|
||||
|
||||
@@ -57,4 +88,14 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? passphrase.getText() : null);
|
||||
}
|
||||
|
||||
private byte[] getMasterFingerprint(Keystore keystore, String passphrase) {
|
||||
try {
|
||||
Keystore copyKeystore = keystore.copy();
|
||||
copyKeystore.getSeed().setPassphrase(passphrase);
|
||||
return copyKeystore.getExtendedMasterPrivateKey().getKey().getFingerprint();
|
||||
} catch(MnemonicException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.event.Event;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTreeTableCell;
|
||||
@@ -13,9 +17,11 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
class LabelCell extends TextFieldTreeTableCell<Entry, String> implements ConfirmationsListener {
|
||||
private static final Logger log = LoggerFactory.getLogger(LabelCell.class);
|
||||
|
||||
private IntegerProperty confirmationsProperty;
|
||||
|
||||
public LabelCell() {
|
||||
super(new DefaultStringConverter());
|
||||
getStyleClass().add("label-cell");
|
||||
@@ -60,6 +66,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
}
|
||||
|
||||
super.commitEdit(label);
|
||||
Platform.runLater(() -> getTreeTableView().requestFocus());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,6 +88,21 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegerProperty getConfirmationsProperty() {
|
||||
if(confirmationsProperty == null) {
|
||||
confirmationsProperty = new SimpleIntegerProperty();
|
||||
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
|
||||
getStyleClass().remove("confirming");
|
||||
confirmationsProperty.unbind();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return confirmationsProperty;
|
||||
}
|
||||
|
||||
private static class LabelContextMenu extends ContextMenu {
|
||||
public LabelContextMenu(Entry entry, String label) {
|
||||
MenuItem copyLabel = new MenuItem("Copy Label");
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.sparrow.io.ImageUtils;
|
||||
import com.sparrowwallet.toucan.LifeHash;
|
||||
import com.sparrowwallet.toucan.LifeHashVersion;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.ImagePattern;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class LifeHashIcon extends Group {
|
||||
private static final Logger log = LoggerFactory.getLogger(LifeHashIcon.class);
|
||||
|
||||
private static final int SIZE = 24;
|
||||
|
||||
private final ObjectProperty<byte[]> dataProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
public LifeHashIcon() {
|
||||
super();
|
||||
|
||||
dataProperty.addListener((observable, oldValue, data) -> {
|
||||
if(data == null) {
|
||||
getChildren().clear();
|
||||
} else if(oldValue == null || !Arrays.equals(oldValue, data)) {
|
||||
LifeHash.Image lifeHashImage = LifeHash.makeFromData(data, LifeHashVersion.VERSION2, 1, false);
|
||||
BufferedImage bufferedImage = LifeHash.getBufferedImage(lifeHashImage);
|
||||
BufferedImage resizedImage = ImageUtils.resizeToImage(bufferedImage, SIZE, SIZE);
|
||||
Image image = SwingFXUtils.toFXImage(resizedImage, null);
|
||||
setImage(image);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setImage(Image image) {
|
||||
getChildren().clear();
|
||||
Rectangle rectangle = new Rectangle(SIZE, SIZE);
|
||||
rectangle.setArcWidth(6);
|
||||
rectangle.setArcHeight(6);
|
||||
rectangle.setFill(new ImagePattern(image));
|
||||
rectangle.setStroke(Color.rgb(65, 72, 77));
|
||||
rectangle.setStrokeWidth(1.0);
|
||||
getChildren().add(rectangle);
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return dataProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<byte[]> dataProperty() {
|
||||
return dataProperty;
|
||||
}
|
||||
|
||||
public void setData(byte[] data) {
|
||||
this.dataProperty.set(data);
|
||||
}
|
||||
|
||||
public void setHex(String hex) {
|
||||
setData(hex == null ? null : Utils.hexToBytes(hex));
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,7 @@ import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
|
||||
@@ -269,8 +266,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
|
||||
}
|
||||
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a USB keystore");
|
||||
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB && !wallet.getKeystores().get(0).getWalletModel().isCard()) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a connected keystore");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +292,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private boolean isValidAddress() {
|
||||
try {
|
||||
Address address = getAddress();
|
||||
return address.getScriptType() != ScriptType.P2TR && address.getScriptType().isAllowed(PolicyType.SINGLE);
|
||||
return address.getScriptType() != ScriptType.P2TR && (address.getScriptType().isAllowed(PolicyType.SINGLE) || address.getScriptType() == ScriptType.P2SH);
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -319,8 +316,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
} else {
|
||||
signUnencryptedKeystore(signingWallet);
|
||||
}
|
||||
} else if(signingWallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
signUsbKeystore(signingWallet);
|
||||
} else if(signingWallet.containsSource(KeystoreSource.HW_USB) || wallet.getKeystores().get(0).getWalletModel().isCard()) {
|
||||
signDeviceKeystore(signingWallet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,10 +336,10 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private void signUsbKeystore(Wallet usbWallet) {
|
||||
List<String> fingerprints = List.of(usbWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
||||
KeyDerivation fullDerivation = usbWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
|
||||
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, usbWallet, message.getText().trim(), fullDerivation);
|
||||
private void signDeviceKeystore(Wallet deviceWallet) {
|
||||
List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
||||
KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
|
||||
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
|
||||
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
||||
if(optSignature.isPresent()) {
|
||||
signature.clear();
|
||||
|
||||
@@ -2,8 +2,11 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.PayNymImageLoadedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymService;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.concurrent.Service;
|
||||
@@ -128,6 +131,7 @@ public class PayNymAvatar extends StackPane {
|
||||
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);
|
||||
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
|
||||
return image;
|
||||
} catch(Exception e) {
|
||||
log.debug("Error loading PayNym avatar", e);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.io.Files;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
@@ -14,7 +15,10 @@ import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.CardApi;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.ActionEvent;
|
||||
@@ -42,7 +46,8 @@ import tornadofx.control.Form;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -96,6 +101,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
HBox.setHgrow(key, Priority.ALWAYS);
|
||||
keyField.getInputs().add(keyBox);
|
||||
|
||||
if(CardApi.isReaderAvailable()) {
|
||||
VBox cardButtonBox = new VBox(5);
|
||||
Button cardKey = new Button("", getGlyph(FontAwesome5.Glyph.WIFI));
|
||||
cardKey.setOnAction(event -> unsealPrivateKey());
|
||||
cardButtonBox.getChildren().add(cardKey);
|
||||
keyBox.getChildren().add(cardButtonBox);
|
||||
}
|
||||
|
||||
Field keyScriptTypeField = new Field();
|
||||
keyScriptTypeField.setText("Script Type:");
|
||||
keyScriptType = new ComboBox<>();
|
||||
@@ -279,6 +292,16 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
}
|
||||
}
|
||||
|
||||
private void unsealPrivateKey() {
|
||||
DeviceUnsealDialog deviceUnsealDialog = new DeviceUnsealDialog(Collections.emptyList());
|
||||
Optional<DeviceUnsealDialog.DevicePrivateKey> optPrivateKey = deviceUnsealDialog.showAndWait();
|
||||
if(optPrivateKey.isPresent()) {
|
||||
DeviceUnsealDialog.DevicePrivateKey devicePrivateKey = optPrivateKey.get();
|
||||
key.setText(devicePrivateKey.privateKey().getPrivateKeyEncoded().toBase58());
|
||||
keyScriptType.setValue(devicePrivateKey.scriptType());
|
||||
}
|
||||
}
|
||||
|
||||
private void createTransaction() {
|
||||
try {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
@@ -286,14 +309,30 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
Address fromAddress = scriptType.getAddress(privateKey.getKey());
|
||||
Address destAddress = getToAddress();
|
||||
|
||||
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress);
|
||||
Date since = null;
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
WalletBirthDateDialog addressScanDateDialog = new WalletBirthDateDialog(null, true);
|
||||
Optional<Date> optSince = addressScanDateDialog.showAndWait();
|
||||
if(optSince.isPresent()) {
|
||||
since = optSince.get();
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress, since);
|
||||
addressUtxosService.setOnSucceeded(successEvent -> {
|
||||
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
|
||||
});
|
||||
addressUtxosService.setOnFailed(failedEvent -> {
|
||||
Throwable rootCause = Throwables.getRootCause(failedEvent.getSource().getException());
|
||||
log.error("Error retrieving outputs for address " + fromAddress, failedEvent.getSource().getException());
|
||||
AppServices.showErrorDialog("Error retrieving outputs for address", failedEvent.getSource().getException().getMessage());
|
||||
AppServices.showErrorDialog("Error retrieving outputs for address", rootCause.getMessage());
|
||||
});
|
||||
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", "/image/sparrow.png", addressUtxosService);
|
||||
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
|
||||
}
|
||||
|
||||
addressUtxosService.start();
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating sweep transaction", e);
|
||||
@@ -321,8 +360,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
|
||||
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||
if(total - fee <= dustThreshold) {
|
||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||
return;
|
||||
feeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
|
||||
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1;
|
||||
|
||||
if(total - fee <= dustThreshold) {
|
||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Transaction transaction = new Transaction();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
public enum QRDensity {
|
||||
NORMAL("Normal", 250),
|
||||
LOW("Low", 80);
|
||||
|
||||
private final String name;
|
||||
private final int maxFragmentLength;
|
||||
|
||||
QRDensity(String name, int maxFragmentLength) {
|
||||
this.name = name;
|
||||
this.maxFragmentLength = maxFragmentLength;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getMaxFragmentLength() {
|
||||
return maxFragmentLength;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.sparrowwallet.hummingbird.LegacyUREncoder;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.UREncoder;
|
||||
@@ -28,13 +29,13 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class QRDisplayDialog extends Dialog<UR> {
|
||||
private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class);
|
||||
|
||||
private static final int MIN_FRAGMENT_LENGTH = 10;
|
||||
private static final int MAX_FRAGMENT_LENGTH = 250;
|
||||
|
||||
private static final int ANIMATION_PERIOD_MILLIS = 200;
|
||||
|
||||
@@ -42,17 +43,20 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
private static final int QR_HEIGHT = 480;
|
||||
|
||||
private final UR ur;
|
||||
private final UREncoder encoder;
|
||||
private UREncoder encoder;
|
||||
|
||||
private final ImageView qrImageView;
|
||||
|
||||
private AnimateQRService animateQRService;
|
||||
private String currentPart;
|
||||
|
||||
private boolean addLegacyEncodingOption;
|
||||
private boolean useLegacyEncoding;
|
||||
private String[] legacyParts;
|
||||
private int legacyPartIndex;
|
||||
|
||||
private static boolean initialDensityChange;
|
||||
|
||||
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
|
||||
this(UR.fromBytes(type, data), addLegacyEncodingOption);
|
||||
}
|
||||
@@ -63,7 +67,8 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
|
||||
public QRDisplayDialog(UR ur, boolean addLegacyEncodingOption) {
|
||||
this.ur = ur;
|
||||
this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0);
|
||||
this.addLegacyEncodingOption = addLegacyEncodingOption;
|
||||
this.encoder = new UREncoder(ur, Config.get().getQrDensity().getMaxFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
|
||||
|
||||
final DialogPane dialogPane = new QRDisplayDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
@@ -88,6 +93,9 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
if(addLegacyEncodingOption) {
|
||||
final ButtonType legacyEncodingButtonType = new javafx.scene.control.ButtonType("Use Legacy Encoding (Cobo Vault)", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
|
||||
} else {
|
||||
final ButtonType densityButtonType = new javafx.scene.control.ButtonType("Change Density", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(densityButtonType);
|
||||
}
|
||||
|
||||
dialogPane.setPrefWidth(40 + QR_WIDTH + 40);
|
||||
@@ -201,6 +209,20 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
}
|
||||
}
|
||||
|
||||
private void changeQRDensity() {
|
||||
if(animateQRService != null) {
|
||||
animateQRService.cancel();
|
||||
}
|
||||
|
||||
this.encoder = new UREncoder(ur, Config.get().getQrDensity().getMaxFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
|
||||
nextPart();
|
||||
if(encoder.isSinglePart()) {
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else {
|
||||
createAnimateQRService();
|
||||
}
|
||||
}
|
||||
|
||||
private class AnimateQRService extends ScheduledService<Boolean> {
|
||||
@Override
|
||||
protected Task<Boolean> createTask() {
|
||||
@@ -220,18 +242,44 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
ToggleButton legacy = new ToggleButton(buttonType.getText());
|
||||
legacy.setGraphicTextGap(5);
|
||||
setLegacyGraphic(legacy, false);
|
||||
if(addLegacyEncodingOption) {
|
||||
ToggleButton legacy = new ToggleButton(buttonType.getText());
|
||||
legacy.setGraphicTextGap(5);
|
||||
setLegacyGraphic(legacy, false);
|
||||
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(legacy, buttonData);
|
||||
legacy.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setUseLegacyEncoding(newValue);
|
||||
setLegacyGraphic(legacy, newValue);
|
||||
});
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(legacy, buttonData);
|
||||
legacy.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setUseLegacyEncoding(newValue);
|
||||
setLegacyGraphic(legacy, newValue);
|
||||
});
|
||||
|
||||
return legacy;
|
||||
return legacy;
|
||||
} else {
|
||||
Button density = new Button(buttonType.getText());
|
||||
density.setPrefWidth(160);
|
||||
density.setGraphicTextGap(5);
|
||||
updateDensityButton(density);
|
||||
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(density, buttonData);
|
||||
density.setOnAction(event -> {
|
||||
if(!initialDensityChange && !encoder.isSinglePart()) {
|
||||
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Discard progress?", "Changing the QR code density means any progress on the receiving device must be discarded. Proceed?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
initialDensityChange = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Config.get().setQrDensity(Config.get().getQrDensity() == QRDensity.NORMAL ? QRDensity.LOW : QRDensity.NORMAL);
|
||||
updateDensityButton(density);
|
||||
changeQRDensity();
|
||||
});
|
||||
|
||||
return density;
|
||||
}
|
||||
}
|
||||
|
||||
return super.createButton(buttonType);
|
||||
@@ -244,6 +292,15 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDensityButton(Button density) {
|
||||
density.setText(Config.get().getQrDensity() == QRDensity.NORMAL ? "Decrease Density" : "Increase Density");
|
||||
if(Config.get().getQrDensity() == QRDensity.NORMAL) {
|
||||
density.setGraphic(getGlyph(FontAwesome5.Glyph.MAGNIFYING_GLASS_PLUS));
|
||||
} else {
|
||||
density.setGraphic(getGlyph(FontAwesome5.Glyph.MAGNIFYING_GLASS_MINUS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -106,13 +108,17 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
|
||||
protected void setDescription(String text) {
|
||||
descriptionLabel.getStyleClass().remove("description-error");
|
||||
descriptionLabel.getStyleClass().add("description-label");
|
||||
if(!descriptionLabel.getStyleClass().contains("description-label")) {
|
||||
descriptionLabel.getStyleClass().add("description-label");
|
||||
}
|
||||
descriptionLabel.setText(text);
|
||||
}
|
||||
|
||||
protected void setError(String title, String detail) {
|
||||
descriptionLabel.getStyleClass().remove("description-label");
|
||||
descriptionLabel.getStyleClass().add("description-error");
|
||||
if(!descriptionLabel.getStyleClass().contains("description-error")) {
|
||||
descriptionLabel.getStyleClass().add("description-error");
|
||||
}
|
||||
descriptionLabel.setText(title);
|
||||
if(detail != null && !detail.isEmpty()) {
|
||||
setContent(getContentBox(detail));
|
||||
@@ -159,4 +165,17 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected static int getAccount(Wallet wallet, KeyDerivation requiredDerivation) {
|
||||
if(wallet == null || requiredDerivation == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int account = wallet.getScriptType().getAccount(requiredDerivation.getDerivationPath());
|
||||
if(account < 0) {
|
||||
account = 0;
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ public class TrayManager {
|
||||
miStage.setFont(Font.decode(null).deriveFont(Font.BOLD));
|
||||
miStage.addActionListener(e -> Platform.runLater(() -> {
|
||||
stage.show();
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.setAlwaysOnTop(false);
|
||||
EventQueue.invokeLater(() -> {
|
||||
popupMenu.remove(miStage);
|
||||
|
||||
|
||||
@@ -21,12 +21,12 @@ import java.util.Date;
|
||||
public class WalletBirthDateDialog extends Dialog<Date> {
|
||||
private final DatePicker birthDatePicker;
|
||||
|
||||
public WalletBirthDateDialog(Date birthDate) {
|
||||
public WalletBirthDateDialog(Date birthDate, boolean singleAddress) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle("Wallet Birth Date");
|
||||
dialogPane.setHeaderText("Select an approximate date earlier than the first wallet transaction:");
|
||||
setTitle(singleAddress ? "Address Scan Start Date" : "Wallet Birth Date");
|
||||
dialogPane.setHeaderText("Select an approximate date earlier than the first " + (singleAddress ? "" : "wallet") + " transaction:");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(420);
|
||||
@@ -58,7 +58,7 @@ public class WalletBirthDateDialog extends Dialog<Date> {
|
||||
));
|
||||
});
|
||||
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rescan Wallet", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType(singleAddress ? "Scan Address" : "Rescan Wallet", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> birthDatePicker.getValue() == null, birthDatePicker.valueProperty());
|
||||
|
||||
@@ -42,9 +42,9 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
|
||||
List<WalletExport> exporters;
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow());
|
||||
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels());
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(), new Descriptor(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
|
||||
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(), new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels());
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ImageUtils;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.ImagePattern;
|
||||
import javafx.scene.shape.Circle;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
public class WalletIcon extends StackPane {
|
||||
public static final String PROTOCOL = "walleticon";
|
||||
private static final String QUERY = "icon";
|
||||
public static final int WIDTH = 15;
|
||||
public static final int HEIGHT = 15;
|
||||
public static final int SAVE_WIDTH = WIDTH * 2;
|
||||
public static final int SAVE_HEIGHT = HEIGHT * 2;
|
||||
|
||||
private final Storage storage;
|
||||
private final ObjectProperty<Wallet> walletProperty = new SimpleObjectProperty<>();
|
||||
private final Keystore keystore;
|
||||
private final Glyph fallbackIcon;
|
||||
|
||||
public WalletIcon(Storage storage, Wallet wallet) {
|
||||
this(storage, wallet, null, null);
|
||||
}
|
||||
|
||||
public WalletIcon(Storage storage, Wallet wallet, Keystore keystore, Glyph fallbackIcon) {
|
||||
super();
|
||||
this.storage = storage;
|
||||
this.keystore = keystore;
|
||||
this.fallbackIcon = fallbackIcon;
|
||||
setPrefSize(WIDTH, HEIGHT);
|
||||
walletProperty.addListener((observable, oldValue, newValue) -> {
|
||||
refresh();
|
||||
});
|
||||
walletProperty.set(wallet);
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
Wallet wallet = getWallet();
|
||||
|
||||
getChildren().clear();
|
||||
if(wallet.getWalletConfig() != null && wallet.getWalletConfig().getIconData() != null) {
|
||||
String walletId = storage.getWalletId(wallet);
|
||||
if(AppServices.get().getWallet(walletId) != null) {
|
||||
addWalletIcon(walletId);
|
||||
} else {
|
||||
Platform.runLater(() -> addWalletIcon(walletId));
|
||||
}
|
||||
} else if(this.keystore != null || wallet.getKeystores().size() == 1) {
|
||||
Keystore keystore = this.keystore == null ? wallet.getKeystores().get(0) : this.keystore;
|
||||
if(keystore.getSource() == KeystoreSource.HW_USB || keystore.getSource() == KeystoreSource.HW_AIRGAPPED) {
|
||||
WalletModel walletModel = keystore.getWalletModel();
|
||||
|
||||
SVGImage svgImage;
|
||||
if(Config.get().getTheme() == Theme.DARK) {
|
||||
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon-invert.svg");
|
||||
} else {
|
||||
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon.svg");
|
||||
}
|
||||
|
||||
if(svgImage != null) {
|
||||
getChildren().add(svgImage);
|
||||
return;
|
||||
}
|
||||
|
||||
Image image = null;
|
||||
if(Config.get().getTheme() == Theme.DARK) {
|
||||
image = loadImage("image/" + walletModel.getType() + "-icon-invert.png");
|
||||
}
|
||||
|
||||
if(image == null) {
|
||||
image = loadImage("image/" + walletModel.getType() + "-icon.png");
|
||||
}
|
||||
|
||||
if(image == null) {
|
||||
image = loadImage("image/" + walletModel.getType() + ".png");
|
||||
}
|
||||
|
||||
if(image != null && !image.isError()) {
|
||||
ImageView imageView = new ImageView(image);
|
||||
getChildren().add(imageView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(getChildren().isEmpty()) {
|
||||
Glyph glyph = fallbackIcon;
|
||||
if(glyph == null) {
|
||||
glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
|
||||
}
|
||||
glyph.setFontSize(10.0);
|
||||
getChildren().add(glyph);
|
||||
}
|
||||
}
|
||||
|
||||
private SVGImage loadSVGImage(String imageName) {
|
||||
try {
|
||||
URL url = AppServices.class.getResource(imageName);
|
||||
if(url != null) {
|
||||
return SVGLoader.load(url);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Image loadImage(String imageName) {
|
||||
try {
|
||||
return new Image(imageName, 15, 15, true, true);
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addWalletIcon(String walletId) {
|
||||
Image image = new Image(PROTOCOL + ":" + walletId + "?" + QUERY, WIDTH, HEIGHT, true, false);
|
||||
getChildren().clear();
|
||||
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
|
||||
circle.setFill(new ImagePattern(image));
|
||||
getChildren().add(circle);
|
||||
}
|
||||
|
||||
public boolean addFailure() {
|
||||
if(getChildren().stream().noneMatch(node -> node.getStyleClass().contains("failure"))) {
|
||||
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
failGlyph.setFontSize(10);
|
||||
failGlyph.getStyleClass().add("failure");
|
||||
getChildren().add(failGlyph);
|
||||
StackPane.setAlignment(failGlyph, Pos.TOP_RIGHT);
|
||||
failGlyph.setTranslateX(5);
|
||||
failGlyph.setTranslateY(-4);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void removeFailure() {
|
||||
getChildren().removeIf(node -> node.getStyleClass().contains("failure"));
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return walletProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Wallet> walletProperty() {
|
||||
return walletProperty;
|
||||
}
|
||||
|
||||
public void setWallet(Wallet wallet) {
|
||||
this.walletProperty.set(wallet);
|
||||
}
|
||||
|
||||
public static class WalletIconStreamHandler extends URLStreamHandler {
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) throws IOException {
|
||||
return new URLConnection(url) {
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
//Nothing required
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
String walletId = url.getPath();
|
||||
String query = url.getQuery();
|
||||
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
if(wallet == null) {
|
||||
throw new IOException("Cannot find wallet for wallet id " + walletId);
|
||||
}
|
||||
|
||||
if(query.startsWith(QUERY)) {
|
||||
if(wallet.getWalletConfig() == null || wallet.getWalletConfig().getIconData() == null) {
|
||||
throw new IOException("No icon data for " + walletId);
|
||||
}
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(wallet.getWalletConfig().getIconData());
|
||||
if(query.endsWith("@2x")) {
|
||||
return bais;
|
||||
} else {
|
||||
return ImageUtils.resize(bais, WalletIcon.WIDTH, WalletIcon.HEIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
throw new MalformedURLException("Cannot load url " + url);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
@@ -16,6 +17,7 @@ import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
@@ -24,7 +26,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
private final Accordion importAccordion;
|
||||
private final Button scanButton;
|
||||
|
||||
public WalletImportDialog() {
|
||||
public WalletImportDialog(List<WalletForm> selectedWalletForms) {
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
@@ -57,7 +59,10 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
}
|
||||
}
|
||||
|
||||
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
|
||||
List<WalletImport> walletImporters = new ArrayList<>(List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow()));
|
||||
if(!selectedWalletForms.isEmpty()) {
|
||||
walletImporters.add(new WalletLabels(selectedWalletForms));
|
||||
}
|
||||
for(WalletImport importer : walletImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileWalletImportPane importPane = new FileWalletImportPane(importer);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
|
||||
import com.sparrowwallet.sparrow.net.TlsServerException;
|
||||
|
||||
public class ConnectionFailedEvent {
|
||||
@@ -20,6 +21,12 @@ public class ConnectionFailedEvent {
|
||||
|
||||
Throwable cause = (exception.getCause() != null ? exception.getCause() : exception);
|
||||
cause = (cause.getCause() != null ? cause.getCause() : cause);
|
||||
|
||||
if(cause instanceof JsonRpcException jsonRpcException && jsonRpcException.getErrorMessage() != null) {
|
||||
return jsonRpcException.getErrorMessage().getMessage() +
|
||||
(jsonRpcException.getErrorMessage().getData() != null ? " (" + jsonRpcException.getErrorMessage().getData().asText() + ")" : "");
|
||||
}
|
||||
|
||||
String message = splitCamelCase(cause.getClass().getSimpleName().replace("Exception", "Error"));
|
||||
return message + " (" + cause.getMessage() + ")";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class CormorantPruneStatusEvent extends CormorantStatusEvent {
|
||||
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd");
|
||||
|
||||
private final Wallet wallet;
|
||||
private final Date scanDate;
|
||||
private final Date pruneDate;
|
||||
private final boolean legacyWalletExists;
|
||||
|
||||
public CormorantPruneStatusEvent(String status, Wallet wallet, Date scanDate, Date pruneDate, boolean legacyWalletExists) {
|
||||
super(status);
|
||||
this.wallet = wallet;
|
||||
this.scanDate = scanDate;
|
||||
this.pruneDate = pruneDate;
|
||||
this.legacyWalletExists = legacyWalletExists;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFor(Wallet wallet) {
|
||||
return this.wallet == wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public Date getScanDate() {
|
||||
return scanDate;
|
||||
}
|
||||
|
||||
public String getScanDateAsString() {
|
||||
return DATE_FORMAT.format(scanDate);
|
||||
}
|
||||
|
||||
public Date getPruneDate() {
|
||||
return pruneDate;
|
||||
}
|
||||
|
||||
public String getPruneDateAsString() {
|
||||
return DATE_FORMAT.format(pruneDate);
|
||||
}
|
||||
|
||||
public boolean legacyWalletExists() {
|
||||
return legacyWalletExists;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Set;
|
||||
|
||||
public class CormorantScanStatusEvent extends CormorantStatusEvent {
|
||||
private final Set<Wallet> scanningWallets;
|
||||
private final int progress;
|
||||
private final Duration remainingDuration;
|
||||
|
||||
public CormorantScanStatusEvent(String status, Set<Wallet> scanningWallets, int progress, Duration remainingDuration) {
|
||||
super(status);
|
||||
this.scanningWallets = scanningWallets;
|
||||
this.progress = progress;
|
||||
this.remainingDuration = remainingDuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFor(Wallet wallet) {
|
||||
return scanningWallets.contains(wallet);
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return progress == 100;
|
||||
}
|
||||
|
||||
public Duration getRemaining() {
|
||||
return remainingDuration;
|
||||
}
|
||||
|
||||
public String getRemainingAsString() {
|
||||
if(remainingDuration != null) {
|
||||
if(progress < 30) {
|
||||
return Math.round((double)remainingDuration.toSeconds() / 60) + "m";
|
||||
} else {
|
||||
return remainingDuration.toMinutesPart() + "m " + remainingDuration.toSecondsPart() + "s";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public abstract class CormorantStatusEvent {
|
||||
private final String status;
|
||||
|
||||
public CormorantStatusEvent(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public abstract boolean isFor(Wallet wallet);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class CormorantSyncStatusEvent extends CormorantStatusEvent {
|
||||
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm");
|
||||
|
||||
private final int progress;
|
||||
private final Date tip;
|
||||
|
||||
public CormorantSyncStatusEvent(String status, int progress, Date tip) {
|
||||
super(status);
|
||||
this.progress = progress;
|
||||
this.tip = tip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFor(Wallet wallet) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return progress == 100;
|
||||
}
|
||||
|
||||
public Date getTip() {
|
||||
return tip;
|
||||
}
|
||||
|
||||
public String getTipAsString() {
|
||||
return tip == null ? "" : DATE_FORMAT.format(tip);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
|
||||
public class DeviceAddressEvent {
|
||||
private final Address address;
|
||||
|
||||
public DeviceAddressEvent(Address address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public Address getAddress() {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
|
||||
public class DeviceGetPrivateKeyEvent {
|
||||
private final ECKey privateKey;
|
||||
private final ScriptType scriptType;
|
||||
|
||||
public DeviceGetPrivateKeyEvent(ECKey privateKey, ScriptType scriptType) {
|
||||
this.privateKey = privateKey;
|
||||
this.scriptType = scriptType;
|
||||
}
|
||||
|
||||
public ECKey getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
public ScriptType getScriptType() {
|
||||
return scriptType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import javafx.scene.image.Image;
|
||||
|
||||
public class PayNymImageLoadedEvent {
|
||||
private final PaymentCode paymentCode;
|
||||
private final Image image;
|
||||
|
||||
public PayNymImageLoadedEvent(PaymentCode paymentCode, Image image) {
|
||||
this.paymentCode = paymentCode;
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
public PaymentCode getPaymentCode() {
|
||||
return paymentCode;
|
||||
}
|
||||
|
||||
public Image getImage() {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletConfigChangedEvent extends WalletChangedEvent {
|
||||
public WalletConfigChangedEvent(Wallet wallet) {
|
||||
super(wallet);
|
||||
}
|
||||
}
|
||||
@@ -11,22 +11,29 @@ import java.util.*;
|
||||
public class WalletEntryLabelsChangedEvent extends WalletChangedEvent {
|
||||
//Contains the changed entry mapped to the entry that changed it, if changed recursively (otherwise null)
|
||||
private final Map<Entry, Entry> entrySourceMap;
|
||||
private final boolean propagate;
|
||||
|
||||
public WalletEntryLabelsChangedEvent(Wallet wallet, Entry entry) {
|
||||
this(wallet, List.of(entry));
|
||||
}
|
||||
|
||||
public WalletEntryLabelsChangedEvent(Wallet wallet, List<Entry> entries) {
|
||||
this(wallet, entries, true);
|
||||
}
|
||||
|
||||
public WalletEntryLabelsChangedEvent(Wallet wallet, List<Entry> entries, boolean propagate) {
|
||||
super(wallet);
|
||||
this.entrySourceMap = new LinkedHashMap<>();
|
||||
for(Entry entry : entries) {
|
||||
entrySourceMap.put(entry, null);
|
||||
}
|
||||
this.propagate = propagate;
|
||||
}
|
||||
|
||||
public WalletEntryLabelsChangedEvent(Wallet wallet, Map<Entry, Entry> entrySourceMap) {
|
||||
super(wallet);
|
||||
this.entrySourceMap = entrySourceMap;
|
||||
this.propagate = true;
|
||||
}
|
||||
|
||||
public Collection<Entry> getEntries() {
|
||||
@@ -36,4 +43,8 @@ public class WalletEntryLabelsChangedEvent extends WalletChangedEvent {
|
||||
public Entry getSource(Entry entry) {
|
||||
return entrySourceMap.get(entry);
|
||||
}
|
||||
|
||||
public boolean propagate() {
|
||||
return propagate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public class WalletNodeHistoryChangedEvent {
|
||||
Wallet notificationWallet = wallet.getNotificationWallet();
|
||||
if(notificationWallet != null) {
|
||||
WalletNode notificationNode = notificationWallet.getNode(KeyPurpose.NOTIFICATION);
|
||||
if(ElectrumServer.getScriptHash(notificationWallet, notificationNode).equals(scriptHash)) {
|
||||
if(ElectrumServer.getScriptHash(notificationNode).equals(scriptHash)) {
|
||||
return notificationNode;
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ public class WalletNodeHistoryChangedEvent {
|
||||
private WalletNode getWalletNode(Wallet wallet, KeyPurpose keyPurpose) {
|
||||
WalletNode purposeNode = wallet.getNode(keyPurpose);
|
||||
for(WalletNode addressNode : new ArrayList<>(purposeNode.getChildren())) {
|
||||
if(ElectrumServer.getScriptHash(wallet, addressNode).equals(scriptHash)) {
|
||||
if(ElectrumServer.getScriptHash(addressNode).equals(scriptHash)) {
|
||||
return addressNode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ public class FontAwesome5 extends GlyphFont {
|
||||
LINK('\uf0c1'),
|
||||
LOCK('\uf023'),
|
||||
LOCK_OPEN('\uf3c1'),
|
||||
MAGNIFYING_GLASS_PLUS('\uf00e'),
|
||||
MAGNIFYING_GLASS_MINUS('\uf010'),
|
||||
MINUS_CIRCLE('\uf056'),
|
||||
PEN_FANCY('\uf5ac'),
|
||||
PLUS('\uf067'),
|
||||
@@ -80,7 +82,8 @@ public class FontAwesome5 extends GlyphFont {
|
||||
USER_PLUS('\uf234'),
|
||||
USER_SLASH('\uf506'),
|
||||
WALLET('\uf555'),
|
||||
WEIGHT('\uf496');
|
||||
WEIGHT('\uf496'),
|
||||
WIFI('\uf1eb');
|
||||
|
||||
private final char ch;
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.io.ckcard.CkCardApi;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import javax.smartcardio.CardTerminals;
|
||||
import javax.smartcardio.TerminalFactory;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class CardApi {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardApi.class);
|
||||
|
||||
private static File[] LINUX_PCSC_LIBS = new File[] {
|
||||
new File("/usr/lib/libpcsclite.so.1"),
|
||||
new File("/usr/local/lib/libpcsclite.so.1"),
|
||||
new File("/lib/x86_64-linux-gnu/libpcsclite.so.1"),
|
||||
new File("/lib/aarch64-linux-gnu/libpcsclite.so.1")};
|
||||
|
||||
private static boolean initialized;
|
||||
|
||||
public static List<WalletModel> getConnectedCards() throws CardException {
|
||||
try {
|
||||
CkCardApi ckCardApi = new CkCardApi(null, null);
|
||||
return List.of(ckCardApi.getCardType());
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public static CardApi getCardApi(WalletModel walletModel, String pin) throws CardException {
|
||||
if(walletModel == WalletModel.TAPSIGNER || walletModel == WalletModel.SATSCARD) {
|
||||
return new CkCardApi(walletModel, pin);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Cannot create card API for " + walletModel.toDisplayString());
|
||||
}
|
||||
|
||||
public abstract boolean isInitialized() throws CardException;
|
||||
|
||||
public abstract void initialize(int slot, byte[] entropy) throws CardException;
|
||||
|
||||
public abstract WalletModel getCardType() throws CardException;
|
||||
|
||||
public abstract ScriptType getDefaultScriptType();
|
||||
|
||||
public abstract int getCurrentSlot() throws CardException;
|
||||
|
||||
public abstract Service<Void> getAuthDelayService() throws CardException;
|
||||
|
||||
public abstract boolean requiresBackup() throws CardException;
|
||||
|
||||
public abstract Service<String> getBackupService();
|
||||
|
||||
public abstract boolean changePin(String newPin) throws CardException;
|
||||
|
||||
public abstract Keystore getKeystore() throws CardException;
|
||||
|
||||
public abstract Service<Void> getInitializationService(byte[] entropy, StringProperty messageProperty);
|
||||
|
||||
public abstract Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty);
|
||||
|
||||
public abstract Service<PSBT> getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty);
|
||||
|
||||
public abstract Service<String> getSignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty);
|
||||
|
||||
public abstract Service<ECKey> getPrivateKeyService(Integer slot, StringProperty messageProperty);
|
||||
|
||||
public abstract Service<Address> getAddressService(StringProperty messageProperty);
|
||||
|
||||
public abstract void disconnect();
|
||||
|
||||
public static boolean isReaderAvailable() {
|
||||
setLibrary();
|
||||
|
||||
try {
|
||||
TerminalFactory tf = TerminalFactory.getDefault();
|
||||
return !tf.terminals().list().isEmpty();
|
||||
} catch(Exception e) {
|
||||
Throwable cause = Throwables.getRootCause(e);
|
||||
if(cause.getMessage().equals("SCARD_E_NO_SERVICE")) {
|
||||
recoverNoService();
|
||||
} else {
|
||||
log.error("Error detecting card terminals", e);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void recoverNoService() {
|
||||
try {
|
||||
Class<?> pcscterminal = Class.forName("sun.security.smartcardio.PCSCTerminals");
|
||||
Field contextId = pcscterminal.getDeclaredField("contextId");
|
||||
contextId.setAccessible(true);
|
||||
|
||||
if(contextId.getLong(pcscterminal) != 0L)
|
||||
{
|
||||
// First get a new context value
|
||||
Class<?> pcsc = Class.forName("sun.security.smartcardio.PCSC");
|
||||
Method SCardEstablishContext = pcsc.getDeclaredMethod(
|
||||
"SCardEstablishContext",
|
||||
Integer.TYPE);
|
||||
SCardEstablishContext.setAccessible(true);
|
||||
|
||||
Field SCARD_SCOPE_USER = pcsc.getDeclaredField("SCARD_SCOPE_USER");
|
||||
SCARD_SCOPE_USER.setAccessible(true);
|
||||
|
||||
long newId = ((Long)SCardEstablishContext.invoke(pcsc,
|
||||
new Object[] { SCARD_SCOPE_USER.getInt(pcsc) }
|
||||
));
|
||||
contextId.setLong(pcscterminal, newId);
|
||||
|
||||
|
||||
// Then clear the terminals in cache
|
||||
TerminalFactory factory = TerminalFactory.getDefault();
|
||||
CardTerminals terminals = factory.terminals();
|
||||
Field fieldTerminals = pcscterminal.getDeclaredField("terminals");
|
||||
fieldTerminals.setAccessible(true);
|
||||
Class<?> classMap = Class.forName("java.util.Map");
|
||||
Method clearMap = classMap.getDeclaredMethod("clear");
|
||||
|
||||
clearMap.invoke(fieldTerminals.get(terminals));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Failed to recover card service", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setLibrary() {
|
||||
if(!initialized && Platform.getCurrent() == Platform.UNIX) {
|
||||
for(File lib : LINUX_PCSC_LIBS) {
|
||||
if(lib.exists()) {
|
||||
System.setProperty("sun.security.smartcardio.library", lib.getAbsolutePath());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
|
||||
public class CardAuthorizationException extends CardException {
|
||||
public CardAuthorizationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CardAuthorizationException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public CardAuthorizationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
|
||||
public interface CardImport extends ImportExport {
|
||||
boolean isInitialized() throws CardException;
|
||||
void initialize(String pin, byte[] chainCode, StringProperty messageProperty) throws CardException;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
|
||||
public class CardSignFailedException extends CardException {
|
||||
public CardSignFailedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CardSignFailedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public CardSignFailedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
|
||||
public class CardUnluckyNumberException extends CardException {
|
||||
public CardUnluckyNumberException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CardUnluckyNumberException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public CardUnluckyNumberException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.Mode;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.control.QRDensity;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
|
||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||
@@ -53,6 +54,7 @@ public class Config {
|
||||
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
|
||||
private File hwi;
|
||||
private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS;
|
||||
private QRDensity qrDensity;
|
||||
private Boolean hdCapture;
|
||||
private String webcamDevice;
|
||||
private ServerType serverType;
|
||||
@@ -62,11 +64,13 @@ public class Config {
|
||||
private CoreAuthType coreAuthType;
|
||||
private File coreDataDir;
|
||||
private String coreAuth;
|
||||
private boolean useLegacyCoreWallet;
|
||||
private Server electrumServer;
|
||||
private List<Server> recentElectrumServers;
|
||||
private File electrumServerCert;
|
||||
private boolean useProxy;
|
||||
private String proxyServer;
|
||||
private boolean autoSwitchProxy = true;
|
||||
private int maxServerTimeout = DEFAULT_MAX_TIMEOUT;
|
||||
private int maxPageSize = DEFAULT_PAGE_SIZE;
|
||||
private boolean usePayNym;
|
||||
@@ -354,6 +358,15 @@ public class Config {
|
||||
return enumerateHwPeriod;
|
||||
}
|
||||
|
||||
public QRDensity getQrDensity() {
|
||||
return qrDensity == null ? QRDensity.NORMAL : qrDensity;
|
||||
}
|
||||
|
||||
public void setQrDensity(QRDensity qrDensity) {
|
||||
this.qrDensity = qrDensity;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Boolean getHdCapture() {
|
||||
return hdCapture;
|
||||
}
|
||||
@@ -501,6 +514,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isUseLegacyCoreWallet() {
|
||||
return useLegacyCoreWallet;
|
||||
}
|
||||
|
||||
public void setUseLegacyCoreWallet(boolean useLegacyCoreWallet) {
|
||||
this.useLegacyCoreWallet = useLegacyCoreWallet;
|
||||
flush();
|
||||
}
|
||||
|
||||
public Server getElectrumServer() {
|
||||
return electrumServer;
|
||||
}
|
||||
@@ -584,6 +606,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isAutoSwitchProxy() {
|
||||
return autoSwitchProxy;
|
||||
}
|
||||
|
||||
public void setAutoSwitchProxy(boolean autoSwitchProxy) {
|
||||
this.autoSwitchProxy = autoSwitchProxy;
|
||||
flush();
|
||||
}
|
||||
|
||||
public int getMaxServerTimeout() {
|
||||
return maxServerTimeout;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Descriptor implements WalletImport, WalletExport {
|
||||
|
||||
@@ -24,10 +25,29 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
try {
|
||||
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
|
||||
bufferedWriter.write("# Receive and change descriptor (BIP389):");
|
||||
bufferedWriter.newLine();
|
||||
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
String outputDescriptorString = outputDescriptor.toString(true);
|
||||
outputStream.write(outputDescriptorString.getBytes(StandardCharsets.UTF_8));
|
||||
outputStream.flush();
|
||||
bufferedWriter.write(outputDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
|
||||
bufferedWriter.write("# Receive descriptor (Bitcoin Core):");
|
||||
bufferedWriter.newLine();
|
||||
OutputDescriptor receiveDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.RECEIVE, null);
|
||||
bufferedWriter.write(receiveDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write("# Change descriptor (Bitcoin Core):");
|
||||
bufferedWriter.newLine();
|
||||
OutputDescriptor changeDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.CHANGE, null);
|
||||
bufferedWriter.write(changeDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
|
||||
bufferedWriter.flush();
|
||||
} catch(Exception e) {
|
||||
throw new ExportException("Error writing output descriptor", e);
|
||||
}
|
||||
@@ -77,14 +97,43 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||
//ignore
|
||||
}
|
||||
|
||||
String outputDescriptor = new BufferedReader(new InputStreamReader(secondClone, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n"));
|
||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(outputDescriptor.trim());
|
||||
return descriptor.toWallet();
|
||||
List<String> paragraphs = getParagraphs(secondClone);
|
||||
for(String paragraph : paragraphs) {
|
||||
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(paragraph);
|
||||
return descriptor.toWallet();
|
||||
}
|
||||
|
||||
throw new ImportException("Could not find an output descriptor in the file");
|
||||
} catch(Exception e) {
|
||||
throw new ImportException("Error importing output descriptor", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> getParagraphs(InputStream inputStream) {
|
||||
List<String> paragraphs = new ArrayList<>();
|
||||
StringBuilder paragraph = new StringBuilder();
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
for(String line : reader.lines().map(String::trim).toArray(String[]::new)) {
|
||||
if(line.isEmpty()) {
|
||||
if(paragraph.length() > 0) {
|
||||
paragraphs.add(paragraph.toString());
|
||||
paragraph.setLength(0);
|
||||
}
|
||||
} else if(line.startsWith("#")) {
|
||||
continue;
|
||||
} else {
|
||||
paragraph.append(line);
|
||||
}
|
||||
}
|
||||
|
||||
if(paragraph.length() > 0) {
|
||||
paragraphs.add(paragraph.toString());
|
||||
}
|
||||
|
||||
return paragraphs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return false;
|
||||
|
||||
@@ -11,6 +11,8 @@ public class Device {
|
||||
private Boolean needsPinSent;
|
||||
private Boolean needsPassphraseSent;
|
||||
private String fingerprint;
|
||||
private boolean card;
|
||||
private String[][] warnings;
|
||||
private String error;
|
||||
|
||||
public String getType() {
|
||||
@@ -69,6 +71,28 @@ public class Device {
|
||||
this.fingerprint = fingerprint;
|
||||
}
|
||||
|
||||
public boolean isCard() {
|
||||
return card;
|
||||
}
|
||||
|
||||
public void setCard(boolean card) {
|
||||
this.card = card;
|
||||
}
|
||||
|
||||
public boolean containsWarning(String warning) {
|
||||
if(warnings != null) {
|
||||
for(String[] warns : warnings) {
|
||||
for(String warn : warns) {
|
||||
if(warn.contains(warning)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getError() {
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import javax.smartcardio.CardNotPresentException;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -34,27 +36,42 @@ public class Hwi {
|
||||
private static final Logger log = LoggerFactory.getLogger(Hwi.class);
|
||||
private static final String HWI_HOME_DIR = "hwi";
|
||||
private static final String HWI_VERSION_PREFIX = "hwi-";
|
||||
private static final String HWI_VERSION = "2.1.1";
|
||||
private static final String HWI_VERSION = "2.2.0";
|
||||
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
|
||||
|
||||
private static boolean isPromptActive = false;
|
||||
|
||||
public List<Device> enumerate(String passphrase) throws ImportException {
|
||||
List<Device> devices = new ArrayList<>();
|
||||
devices.addAll(enumerateUsb(passphrase));
|
||||
devices.addAll(enumerateCard());
|
||||
return devices;
|
||||
}
|
||||
|
||||
private List<Device> enumerateUsb(String passphrase) throws ImportException {
|
||||
String output = null;
|
||||
try {
|
||||
List<String> command;
|
||||
if(passphrase != null && !passphrase.isEmpty()) {
|
||||
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", escape(passphrase), Command.ENUMERATE.toString());
|
||||
if(passphrase != null) {
|
||||
command = new ArrayList<>(List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", escape(passphrase), Command.ENUMERATE.toString()));
|
||||
} else {
|
||||
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString());
|
||||
command = new ArrayList<>(List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString()));
|
||||
}
|
||||
|
||||
addChainType(command, true);
|
||||
|
||||
isPromptActive = true;
|
||||
output = execute(command);
|
||||
Device[] devices = getGson().fromJson(output, Device[].class);
|
||||
if(devices == null) {
|
||||
throw new ImportException("Error scanning, check devices are ready");
|
||||
}
|
||||
//Restore previous (pre v2.2.0) behaviour for Trezor One - don't default to an empty passphrase if one is not supplied
|
||||
Arrays.stream(devices).filter(device -> device.containsWarning("Using default passphrase of the empty string")).forEach(device -> {
|
||||
device.setFingerprint(null);
|
||||
device.setNeedsPassphraseSent(true);
|
||||
device.setError("Passphrase needs to be specified before the fingerprint information can be retrieved");
|
||||
});
|
||||
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);
|
||||
@@ -67,7 +84,7 @@ public class Hwi {
|
||||
throw new ImportException(error.getAsString());
|
||||
} catch(Exception ex) {
|
||||
log.error("Error parsing JSON: " + output, e);
|
||||
throw new ImportException("Error parsing JSON: " + output, e);
|
||||
throw new ImportException("Error scanning" + (output.isEmpty() ? ", check devices are ready" : ": " + output), e);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
@@ -76,6 +93,33 @@ public class Hwi {
|
||||
}
|
||||
}
|
||||
|
||||
private List<Device> enumerateCard() {
|
||||
List<Device> devices = new ArrayList<>();
|
||||
if(CardApi.isReaderAvailable()) {
|
||||
try {
|
||||
List<WalletModel> connectedCards = CardApi.getConnectedCards();
|
||||
for(WalletModel card : connectedCards) {
|
||||
CardApi cardApi = CardApi.getCardApi(card, null);
|
||||
WalletModel walletModel = cardApi.getCardType();
|
||||
|
||||
Device cardDevice = new Device();
|
||||
cardDevice.setType(walletModel.getType());
|
||||
cardDevice.setModel(walletModel);
|
||||
cardDevice.setNeedsPassphraseSent(Boolean.FALSE);
|
||||
cardDevice.setNeedsPinSent(Boolean.FALSE);
|
||||
cardDevice.setCard(true);
|
||||
devices.add(cardDevice);
|
||||
}
|
||||
} catch(CardNotPresentException e) {
|
||||
//ignore
|
||||
} catch(CardException e) {
|
||||
log.error("Error reading card", e);
|
||||
}
|
||||
}
|
||||
|
||||
return devices;
|
||||
}
|
||||
|
||||
public boolean promptPin(Device device) throws ImportException {
|
||||
try {
|
||||
String output = execute(getDeviceCommand(device, Command.PROMPT_PIN));
|
||||
@@ -118,7 +162,7 @@ public class Hwi {
|
||||
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
|
||||
try {
|
||||
String output;
|
||||
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
|
||||
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceCommand(device, passphrase, Command.GET_XPUB, derivationPath));
|
||||
} else {
|
||||
output = execute(getDeviceCommand(device, Command.GET_XPUB, derivationPath));
|
||||
@@ -151,7 +195,7 @@ public class Hwi {
|
||||
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
|
||||
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--desc", descriptor));
|
||||
} else {
|
||||
output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--desc", descriptor));
|
||||
@@ -179,7 +223,7 @@ public class Hwi {
|
||||
try {
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
|
||||
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
|
||||
} else {
|
||||
output = execute(getDeviceArguments(device, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
|
||||
@@ -209,7 +253,7 @@ public class Hwi {
|
||||
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && !passphrase.isEmpty() && device.getModel().externalPassphraseEntry()) {
|
||||
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
|
||||
} else {
|
||||
output = execute(getDeviceArguments(device, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
|
||||
|
||||
@@ -142,7 +142,7 @@ public class IOUtils {
|
||||
pos += data.length;
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.warn("Error overwriting file for deletion " + file.getName(), e);
|
||||
log.warn("Error overwriting file for deletion: " + file.getName(), e);
|
||||
}
|
||||
|
||||
return file.delete();
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.image.Image;
|
||||
import net.coobird.thumbnailator.Thumbnails;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
|
||||
public class ImageUtils {
|
||||
public static byte[] resize(Image image, int width, int height) {
|
||||
return resize(SwingFXUtils.fromFXImage(image, null), width, height);
|
||||
}
|
||||
|
||||
public static byte[] resize(BufferedImage image, int width, int height) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
resize(image, baos, width, height);
|
||||
return baos.toByteArray();
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void resize(BufferedImage image, OutputStream outputStream, int width, int height) throws IOException {
|
||||
resize(Thumbnails.of(image), outputStream, width, height);
|
||||
}
|
||||
|
||||
public static BufferedImage resizeToImage(BufferedImage image, int width, int height) {
|
||||
try {
|
||||
return Thumbnails.of(image).size(width, height).outputQuality(1).asBufferedImage();
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] resize(File file, int width, int height) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
resize(file, baos, width, height);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
public static void resize(File file, OutputStream outputStream, int width, int height) throws IOException {
|
||||
resize(Thumbnails.of(file), outputStream, width, height);
|
||||
}
|
||||
|
||||
public static InputStream resize(InputStream inputStream, int width, int height) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
resize(inputStream, baos, width, height);
|
||||
return new ByteArrayInputStream(baos.toByteArray());
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void resize(InputStream inputStream, OutputStream outputStream, int width, int height) throws IOException {
|
||||
resize(Thumbnails.of(inputStream), outputStream, width, height);
|
||||
}
|
||||
|
||||
private static void resize(Thumbnails.Builder<?> builder, OutputStream outputStream, int width, int height) throws IOException {
|
||||
builder.size(width, height).outputFormat("png").outputQuality(1).toOutputStream(outputStream);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
public class JadeMultisig extends ColdcardMultisig {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Jade Multisig";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.JADE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "Export a QR that allows Jade to import a multisig wallet using the Scan feature.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportFile() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean walletExportRequiresDecryption() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface KeystoreCardImport extends CardImport {
|
||||
Keystore getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException;
|
||||
String getKeystoreImportDescription(int account);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public class PassportMultisig extends ColdcardMultisig {
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file or QR created from New Account > Sparrow > Multisig > QR Code/microSD on your Passport. For existing accounts, use Manage Account > Export by QR/microSD.";
|
||||
return "Import file or QR created from Manage Account > Connect Wallet > Sparrow > Multisig > QR Code/microSD on your Passport.";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -38,7 +38,7 @@ public class PassportMultisig extends ColdcardMultisig {
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "As part of the New Account > Sparrow > Multisig flow, Passport will ask you to scan or import the multisig configuration from Sparrow.";
|
||||
return "As part of the Multisig connection flow, Passport will ask you to scan or import the multisig configuration from Sparrow. To import this configuration manually, go to Settings > Bitcoin > Multisig > Import from QR Code/microSD.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -14,7 +14,7 @@ public class PassportSinglesig extends ColdcardSinglesig {
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file or QR created from New Account > Sparrow > Standard > QR Code/microSD on your Passport. For existing accounts, use Manage Account > Export by QR/microSD.";
|
||||
return "Import file or QR created from Manage Account > Connect Wallet > Sparrow > Single-sig > QR Code/microSD on your Passport.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,10 +31,10 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
try {
|
||||
Wallet exportedWallet = !wallet.isMasterWallet() ? wallet.getMasterWallet() : wallet;
|
||||
PersistenceType persistenceType = exportedWallet.getChildWallets().isEmpty() ? PersistenceType.JSON : PersistenceType.DB;
|
||||
PersistenceType persistenceType = PersistenceType.DB;
|
||||
Persistence persistence = persistenceType.getInstance();
|
||||
Storage storage = AppServices.get().getOpenWallets().get(exportedWallet);
|
||||
File tempFile = File.createTempFile(exportedWallet.getName(), "." + persistenceType.getExtension());
|
||||
File tempFile = File.createTempFile(exportedWallet.getName() + "tmp", "." + persistenceType.getExtension());
|
||||
tempFile.delete();
|
||||
Storage tempStorage = new Storage(persistence, tempFile);
|
||||
tempStorage.setKeyDeriver(storage.getKeyDeriver());
|
||||
@@ -60,15 +60,7 @@ public class Sparrow implements WalletImport, WalletExport {
|
||||
|
||||
@Override
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
try {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Wallet exportedWallet = !wallet.isMasterWallet() ? wallet.getMasterWallet() : wallet;
|
||||
return !exportedWallet.getChildWallets().isEmpty() ? PersistenceType.DB.getExtension() : (storage.isEncrypted() ? "" : PersistenceType.JSON.getExtension());
|
||||
} catch(IOException e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
return "";
|
||||
return PersistenceType.DB.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,8 +8,10 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.SparrowWallet;
|
||||
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
||||
@@ -144,7 +146,15 @@ public class Storage {
|
||||
|
||||
if(wallet.containsMasterPrivateKeys()) {
|
||||
//Derive xpub and master fingerprint from seed, potentially with passphrase
|
||||
Wallet copy = wallet.copy();
|
||||
Wallet copy = wallet.copy(false);
|
||||
if(wallet.isEncrypted()) {
|
||||
if(key == null) {
|
||||
throw new IllegalStateException("Wallet was not encrypted, but seed is");
|
||||
}
|
||||
|
||||
copy.decrypt(key);
|
||||
}
|
||||
|
||||
for(int i = 0; i < copy.getKeystores().size(); i++) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
if(copyKeystore.hasSeed() && copyKeystore.getSeed().getPassphrase() == null) {
|
||||
@@ -165,14 +175,6 @@ public class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
if(wallet.isEncrypted()) {
|
||||
if(key == null) {
|
||||
throw new IllegalStateException("Wallet was not encrypted, but seed is");
|
||||
}
|
||||
|
||||
copy.decrypt(key);
|
||||
}
|
||||
|
||||
if(wallet.isWhirlpoolMasterWallet()) {
|
||||
String walletId = getWalletId(wallet);
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
@@ -263,9 +265,9 @@ public class Storage {
|
||||
persistence.copyWallet(walletFile, outputStream);
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
public boolean delete() {
|
||||
deleteBackups();
|
||||
IOUtils.secureDelete(walletFile);
|
||||
return IOUtils.secureDelete(walletFile);
|
||||
}
|
||||
|
||||
public void deleteBackups() {
|
||||
@@ -732,4 +734,21 @@ public class Storage {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class DeleteWalletService extends ScheduledService<Boolean> {
|
||||
private final Storage storage;
|
||||
|
||||
public DeleteWalletService(Storage storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Boolean> createTask() {
|
||||
return new Task<>() {
|
||||
protected Boolean call() {
|
||||
return storage.delete();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ public interface WalletExport extends ImportExport {
|
||||
String getExportFileExtension(Wallet wallet);
|
||||
boolean isWalletExportScannable();
|
||||
boolean walletExportRequiresDecryption();
|
||||
default boolean isWalletExportFile() {
|
||||
return true;
|
||||
}
|
||||
default boolean exportsAllWallets() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreLabelsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
public class WalletLabels implements WalletImport, WalletExport {
|
||||
private static final Logger log = LoggerFactory.getLogger(WalletLabels.class);
|
||||
|
||||
private final List<WalletForm> walletForms;
|
||||
|
||||
public WalletLabels() {
|
||||
this.walletForms = Collections.emptyList();
|
||||
}
|
||||
|
||||
public WalletLabels(List<WalletForm> walletForms) {
|
||||
this.walletForms = walletForms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted(File file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Wallet Labels";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.LABELS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
|
||||
List<Label> labels = new ArrayList<>();
|
||||
List<Wallet> allWallets = wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets();
|
||||
for(Wallet exportWallet : allWallets) {
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet);
|
||||
String origin = outputDescriptor.toString(true, false, false);
|
||||
|
||||
for(Keystore keystore : exportWallet.getKeystores()) {
|
||||
if(keystore.getLabel() != null && !keystore.getLabel().isEmpty()) {
|
||||
labels.add(new Label(Type.xpub, keystore.getExtendedPublicKey().toString(), keystore.getLabel(), null));
|
||||
}
|
||||
}
|
||||
|
||||
for(BlockTransaction blkTx : exportWallet.getWalletTransactions().values()) {
|
||||
if(blkTx.getLabel() != null && !blkTx.getLabel().isEmpty()) {
|
||||
labels.add(new Label(Type.tx, blkTx.getHashAsString(), blkTx.getLabel(), origin));
|
||||
}
|
||||
}
|
||||
|
||||
for(WalletNode addressNode : exportWallet.getWalletAddresses().values()) {
|
||||
if(addressNode.getLabel() != null && !addressNode.getLabel().isEmpty()) {
|
||||
labels.add(new Label(Type.addr, addressNode.getAddress().toString(), addressNode.getLabel(), null));
|
||||
}
|
||||
}
|
||||
|
||||
for(BlockTransactionHashIndex txo : exportWallet.getWalletTxos().keySet()) {
|
||||
if(txo.getLabel() != null && !txo.getLabel().isEmpty()) {
|
||||
labels.add(new Label(Type.output, txo.toString(), txo.getLabel(), null));
|
||||
}
|
||||
|
||||
if(txo.isSpent() && txo.getSpentBy().getLabel() != null && !txo.getSpentBy().getLabel().isEmpty()) {
|
||||
labels.add(new Label(Type.input, txo.getSpentBy().toString(), txo.getSpentBy().getLabel(), null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
|
||||
for(Label label : labels) {
|
||||
writer.write(gson.toJson(label) + "\n");
|
||||
}
|
||||
|
||||
writer.flush();
|
||||
} catch(Exception e) {
|
||||
log.error("Error exporting labels", e);
|
||||
throw new ExportException("Error exporting labels", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "Exports a file containing labels from this wallet in the BIP329 standard format.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExportFileExtension(Wallet wallet) {
|
||||
return "jsonl";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletExportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean walletExportRequiresDecryption() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return "Imports a file containing labels in the BIP329 standard format to the currently selected wallet.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
if(walletForms.isEmpty()) {
|
||||
throw new IllegalStateException("No wallets to import labels for");
|
||||
}
|
||||
|
||||
Gson gson = new Gson();
|
||||
List<Label> labels = new ArrayList<>();
|
||||
try {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
String line;
|
||||
while((line = reader.readLine()) != null) {
|
||||
Label label;
|
||||
try {
|
||||
label = gson.fromJson(line, Label.class);
|
||||
} catch(Exception e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(label == null || label.type == null || label.ref == null || label.label == null || label.label.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
labels.add(label);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw new ImportException("Error importing labels file", e);
|
||||
}
|
||||
|
||||
Map<Wallet, List<Keystore>> changedWalletKeystores = new LinkedHashMap<>();
|
||||
Map<Wallet, List<Entry>> changedWalletEntries = new LinkedHashMap<>();
|
||||
|
||||
for(WalletForm walletForm : walletForms) {
|
||||
Wallet wallet = walletForm.getWallet();
|
||||
if(!wallet.isValid()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
|
||||
String origin = outputDescriptor.toString(true, false, false);
|
||||
|
||||
List<Entry> transactionEntries = walletForm.getWalletTransactionsEntry().getChildren();
|
||||
List<Entry> addressEntries = new ArrayList<>();
|
||||
addressEntries.addAll(walletForm.getNodeEntry(KeyPurpose.RECEIVE).getChildren());
|
||||
addressEntries.addAll(walletForm.getNodeEntry(KeyPurpose.CHANGE).getChildren());
|
||||
List<Entry> utxoEntries = walletForm.getWalletUtxosEntry().getChildren();
|
||||
|
||||
for(Label label : labels) {
|
||||
if(label.origin != null && !label.origin.equals(origin)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if(label.type == Type.xpub) {
|
||||
for(Keystore keystore : wallet.getKeystores()) {
|
||||
if(keystore.getExtendedPublicKey().toString().equals(label.ref)) {
|
||||
keystore.setLabel(label.label);
|
||||
List<Keystore> changedKeystores = changedWalletKeystores.computeIfAbsent(wallet, w -> new ArrayList<>());
|
||||
changedKeystores.add(keystore);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(label.type == Type.tx) {
|
||||
for(Entry entry : transactionEntries) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
BlockTransaction blkTx = transactionEntry.getBlockTransaction();
|
||||
if(blkTx.getHashAsString().equals(label.ref)) {
|
||||
transactionEntry.getBlockTransaction().setLabel(label.label);
|
||||
transactionEntry.labelProperty().set(label.label);
|
||||
addChangedEntry(changedWalletEntries, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(label.type == Type.addr) {
|
||||
for(Entry addressEntry : addressEntries) {
|
||||
if(addressEntry instanceof NodeEntry nodeEntry) {
|
||||
WalletNode addressNode = nodeEntry.getNode();
|
||||
if(addressNode.getAddress().toString().equals(label.ref)) {
|
||||
nodeEntry.getNode().setLabel(label.label);
|
||||
nodeEntry.labelProperty().set(label.label);
|
||||
addChangedEntry(changedWalletEntries, addressEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(label.type == Type.output || label.type == Type.input) {
|
||||
for(Entry entry : transactionEntries) {
|
||||
for(Entry hashIndexEntry : entry.getChildren()) {
|
||||
if(hashIndexEntry instanceof TransactionHashIndexEntry txioEntry) {
|
||||
BlockTransactionHashIndex reference = txioEntry.getHashIndex();
|
||||
if((label.type == Type.output && txioEntry.getType() == HashIndexEntry.Type.OUTPUT && reference.toString().equals(label.ref))
|
||||
|| (label.type == Type.input && txioEntry.getType() == HashIndexEntry.Type.INPUT && reference.toString().equals(label.ref))) {
|
||||
txioEntry.getHashIndex().setLabel(label.label);
|
||||
txioEntry.labelProperty().set(label.label);
|
||||
addChangedEntry(changedWalletEntries, txioEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for(Entry addressEntry : addressEntries) {
|
||||
for(Entry entry : addressEntry.getChildren()) {
|
||||
updateHashIndexEntryLabel(label, entry);
|
||||
for(Entry spentEntry : entry.getChildren()) {
|
||||
updateHashIndexEntryLabel(label, spentEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
for(Entry entry : utxoEntries) {
|
||||
updateHashIndexEntryLabel(label, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Map.Entry<Wallet, List<Keystore>> walletKeystores : changedWalletKeystores.entrySet()) {
|
||||
Wallet wallet = walletKeystores.getKey();
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
EventManager.get().post(new KeystoreLabelsChangedEvent(wallet, wallet, storage.getWalletId(wallet), walletKeystores.getValue()));
|
||||
}
|
||||
|
||||
for(Map.Entry<Wallet, List<Entry>> walletEntries : changedWalletEntries.entrySet()) {
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(walletEntries.getKey(), walletEntries.getValue(), false));
|
||||
}
|
||||
|
||||
return walletForms.get(0).getWallet();
|
||||
}
|
||||
|
||||
private static void updateHashIndexEntryLabel(Label label, Entry entry) {
|
||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
BlockTransactionHashIndex reference = hashIndexEntry.getHashIndex();
|
||||
if((label.type == Type.output && hashIndexEntry.getType() == HashIndexEntry.Type.OUTPUT && reference.toString().equals(label.ref))
|
||||
|| (label.type == Type.input && hashIndexEntry.getType() == HashIndexEntry.Type.INPUT && reference.toString().equals(label.ref))) {
|
||||
hashIndexEntry.labelProperty().set(label.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addChangedEntry(Map<Wallet, List<Entry>> changedEntries, Entry entry) {
|
||||
List<Entry> entries = changedEntries.computeIfAbsent(entry.getWallet(), wallet -> new ArrayList<>());
|
||||
entries.add(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exportsAllWallets() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private enum Type {
|
||||
tx, addr, pubkey, input, output, xpub
|
||||
}
|
||||
|
||||
private static class Label {
|
||||
public Label(Type type, String ref, String label, String origin) {
|
||||
this.type = type;
|
||||
this.ref = ref;
|
||||
this.label = label;
|
||||
this.origin = origin;
|
||||
}
|
||||
|
||||
Type type;
|
||||
String ref;
|
||||
String label;
|
||||
String origin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardAuthDump extends CardUnseal {
|
||||
boolean tampered;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardBackup extends CardResponse {
|
||||
byte[] data;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CardCerts {
|
||||
List<byte[]> cert_chain;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardChange extends CardResponse {
|
||||
boolean success;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class CardDerive extends CardResponse {
|
||||
byte[] sig;
|
||||
byte[] chain_code;
|
||||
byte[] master_pubkey;
|
||||
byte[] pubkey;
|
||||
|
||||
public ECDSASignature getSignature() {
|
||||
BigInteger r = new BigInteger(1, Arrays.copyOfRange(sig, 0, 32));
|
||||
BigInteger s = new BigInteger(1, Arrays.copyOfRange(sig, 32, 64));
|
||||
return new ECDSASignature(r, s);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
|
||||
public class CardDump extends CardResponse {
|
||||
int slot;
|
||||
boolean used = true;
|
||||
boolean sealed;
|
||||
String address;
|
||||
byte[] pubkey;
|
||||
|
||||
public Address getAddress() throws CardException {
|
||||
try {
|
||||
if(address != null) {
|
||||
return Address.fromString(address);
|
||||
}
|
||||
} catch(InvalidAddressException e) {
|
||||
throw new CardException("Invalid address provided", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.sparrow.io.CardSignFailedException;
|
||||
import com.sparrowwallet.sparrow.io.CardUnluckyNumberException;
|
||||
import org.bitcoin.NativeSecp256k1;
|
||||
import org.bitcoin.NativeSecp256k1Util;
|
||||
import org.bitcoin.Secp256k1Context;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.SignatureException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CardProtocol {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardProtocol.class);
|
||||
|
||||
public static final byte[] OPENDIME_HEADER = "OPENDIME".getBytes(StandardCharsets.UTF_8);
|
||||
public static final List<byte[]> FACTORY_ROOT_KEYS = List.of(Utils.hexToBytes("03028a0e89e70d0ec0d932053a89ab1da7d9182bdc6d2f03e706ee99517d05d9e1"));
|
||||
public static final int MAX_PATH_DEPTH = 8;
|
||||
|
||||
private final CardTransport cardTransport;
|
||||
private final Gson gson;
|
||||
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
private byte[] cardPubkey;
|
||||
private byte[] lastCardNonce;
|
||||
|
||||
public CardProtocol() throws CardException {
|
||||
this.cardTransport = new CardTransport();
|
||||
this.gson = new GsonBuilder().registerTypeAdapter(byte[].class, new ByteArrayToHexTypeAdapter()).create();
|
||||
}
|
||||
|
||||
public CardStatus getStatus() throws CardException {
|
||||
JsonObject status = send("status");
|
||||
CardStatus cardStatus = gson.fromJson(status, CardStatus.class);
|
||||
if(cardPubkey == null) {
|
||||
cardPubkey = cardStatus.pubkey;
|
||||
}
|
||||
|
||||
return cardStatus;
|
||||
}
|
||||
|
||||
public CardCerts getCerts() throws CardException {
|
||||
JsonObject certs = send("certs");
|
||||
return gson.fromJson(certs, CardCerts.class);
|
||||
}
|
||||
|
||||
public void verify() throws CardException {
|
||||
CardCerts cardCerts = getCerts();
|
||||
|
||||
byte[] userNonce = getNonce();
|
||||
byte[] cardNonce = lastCardNonce;
|
||||
JsonObject certs = send("check", Map.of("nonce", userNonce));
|
||||
CardSignature cardSignature = gson.fromJson(certs, CardSignature.class);
|
||||
Sha256Hash verificationData = getVerificationData(cardNonce, userNonce, new byte[0]);
|
||||
ECDSASignature ecdsaSignature = cardSignature.getSignature();
|
||||
if(!ecdsaSignature.verify(verificationData.getBytes(), cardPubkey)) {
|
||||
throw new CardException("Card authentication failure: Provided signature did not match public key");
|
||||
}
|
||||
|
||||
byte[] pubkey = cardPubkey;
|
||||
for(byte[] cert : cardCerts.cert_chain) {
|
||||
Sha256Hash pubkeyHash = Sha256Hash.of(pubkey);
|
||||
|
||||
try {
|
||||
ECKey recoveredKey = ECKey.signedHashToKey(pubkeyHash, cert, false);
|
||||
pubkey = recoveredKey.getPubKey();
|
||||
} catch(SignatureException e) {
|
||||
throw new CardException("Card signature error", e);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] rootPubKey = pubkey;
|
||||
if(FACTORY_ROOT_KEYS.stream().noneMatch(key -> Arrays.equals(key, rootPubKey))) {
|
||||
throw new CardException("Card authentication failure: Could not verify to root certificate");
|
||||
}
|
||||
}
|
||||
|
||||
public CardRead read(String cvc, int currentSlot) throws CardException {
|
||||
byte[] userNonce = getNonce();
|
||||
byte[] cardNonce = lastCardNonce;
|
||||
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("nonce", userNonce);
|
||||
|
||||
JsonObject read;
|
||||
if(cvc == null) {
|
||||
read = send("read", args);
|
||||
} else {
|
||||
read = sendAuth("read", args, cvc);
|
||||
}
|
||||
CardRead cardRead = gson.fromJson(read, CardRead.class);
|
||||
|
||||
ECDSASignature ecdsaSignature = cardRead.getSignature();
|
||||
Sha256Hash verificationData = getVerificationData(cardNonce, userNonce, new byte[] { (byte)currentSlot });
|
||||
|
||||
if(!ecdsaSignature.verify(verificationData.getBytes(), cardRead.pubkey)) {
|
||||
throw new CardException("Card authentication failure: Provided signature did not match public key");
|
||||
}
|
||||
|
||||
return cardRead;
|
||||
}
|
||||
|
||||
public CardSetup setup(String cvc, int slot, byte[] chainCode) throws CardException {
|
||||
if(chainCode == null) {
|
||||
chainCode = Sha256Hash.hashTwice(secureRandom.generateSeed(128));
|
||||
}
|
||||
|
||||
if(chainCode.length != 32) {
|
||||
throw new IllegalArgumentException("Invalid chain code length of " + chainCode.length);
|
||||
}
|
||||
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("slot", slot);
|
||||
args.put("chain_code", chainCode);
|
||||
JsonObject setup = sendAuth("new", args, cvc);
|
||||
return gson.fromJson(setup, CardSetup.class);
|
||||
}
|
||||
|
||||
public CardWait authWait() throws CardException {
|
||||
JsonObject wait = send("wait");
|
||||
return gson.fromJson(wait, CardWait.class);
|
||||
}
|
||||
|
||||
public CardXpub xpub(String cvc, boolean master) throws CardException {
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("master", master);
|
||||
|
||||
JsonObject xpub = sendAuth("xpub", args, cvc);
|
||||
return gson.fromJson(xpub, CardXpub.class);
|
||||
}
|
||||
|
||||
public CardDerive derive(String cvc, List<ChildNumber> path) throws CardException {
|
||||
if(path.stream().anyMatch(childNumber -> !childNumber.isHardened())) {
|
||||
throw new IllegalArgumentException("Derivation path cannot contain unhardened components");
|
||||
}
|
||||
|
||||
if(path.size() > MAX_PATH_DEPTH) {
|
||||
throw new IllegalArgumentException("Derivation path cannot have more than " + MAX_PATH_DEPTH + " components");
|
||||
}
|
||||
|
||||
if(lastCardNonce == null || cardPubkey == null) {
|
||||
getStatus();
|
||||
}
|
||||
|
||||
byte[] userNonce = getNonce();
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("path", path.stream().map(cn -> Integer.toUnsignedLong(cn.i())).collect(Collectors.toList()));
|
||||
args.put("nonce", userNonce);
|
||||
|
||||
JsonObject derive = sendAuth("derive", args, cvc);
|
||||
return gson.fromJson(derive, CardDerive.class);
|
||||
}
|
||||
|
||||
public CardSign sign(String cvc, List<ChildNumber> subpath, Sha256Hash digest) throws CardException {
|
||||
if(subpath.stream().anyMatch(ChildNumber::isHardened)) {
|
||||
throw new IllegalArgumentException("Derivation path cannot contain hardened components");
|
||||
}
|
||||
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("subpath", subpath.stream().map(cn -> Integer.toUnsignedLong(cn.i())).collect(Collectors.toList()));
|
||||
args.put("digest", digest.getBytes());
|
||||
|
||||
for(int attempt = 0; attempt < 5; attempt++) {
|
||||
try {
|
||||
JsonObject sign = sendAuth("sign", args, cvc);
|
||||
CardSign cardSign = gson.fromJson(sign, CardSign.class);
|
||||
if(!cardSign.getSignature().verify(digest.getBytes(), cardSign.pubkey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return cardSign;
|
||||
} catch(CardUnluckyNumberException e) {
|
||||
log.debug("Got unlucky number signing, trying again...");
|
||||
}
|
||||
}
|
||||
|
||||
throw new CardSignFailedException("Failed to sign digest after 5 tries. It's safe to try again.");
|
||||
}
|
||||
|
||||
public CardChange change(String currentCvc, String newCvc) throws CardException {
|
||||
if(newCvc.length() < 6 || newCvc.length() > 32) {
|
||||
throw new IllegalArgumentException("CVC cannot be of length " + newCvc.length());
|
||||
}
|
||||
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("data", newCvc.getBytes(StandardCharsets.UTF_8));
|
||||
JsonObject change = sendAuth("change", args, currentCvc);
|
||||
return gson.fromJson(change, CardChange.class);
|
||||
}
|
||||
|
||||
public CardBackup backup(String cvc) throws CardException {
|
||||
JsonObject backup = sendAuth("backup", new HashMap<>(), cvc);
|
||||
return gson.fromJson(backup, CardBackup.class);
|
||||
}
|
||||
|
||||
public CardUnseal unseal(String cvc, int slot) throws CardException {
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("slot", slot);
|
||||
|
||||
JsonObject unseal = sendAuth("unseal", args, cvc);
|
||||
return gson.fromJson(unseal, CardUnseal.class);
|
||||
}
|
||||
|
||||
public CardDump dump(int slot) throws CardException {
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("slot", slot);
|
||||
|
||||
JsonObject dump = send("dump", args);
|
||||
return gson.fromJson(dump, CardDump.class);
|
||||
}
|
||||
|
||||
public CardAuthDump dump(String cvc, int slot) throws CardException {
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("slot", slot);
|
||||
|
||||
JsonObject dump = sendAuth("dump", args, cvc);
|
||||
return gson.fromJson(dump, CardAuthDump.class);
|
||||
}
|
||||
|
||||
public void disconnect() throws CardException {
|
||||
cardTransport.disconnect();
|
||||
}
|
||||
|
||||
private JsonObject send(String cmd) throws CardException {
|
||||
return send(cmd, Collections.emptyMap());
|
||||
}
|
||||
|
||||
private JsonObject send(String cmd, Map<String, Object> args) throws CardException {
|
||||
JsonObject jsonObject = cardTransport.send(cmd, args);
|
||||
if(jsonObject.get("card_nonce") != null) {
|
||||
lastCardNonce = gson.fromJson(jsonObject, CardResponse.class).card_nonce;
|
||||
}
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
private JsonObject sendAuth(String cmd, Map<String, Object> args, String cvc) throws CardException {
|
||||
byte[] sessionKey = addAuth(cmd, args, cvc);
|
||||
JsonObject jsonObject = send(cmd, args);
|
||||
|
||||
if(jsonObject.get("privkey") != null) {
|
||||
byte[] privKeyBytes = Utils.hexToBytes(jsonObject.get("privkey").getAsString());
|
||||
jsonObject.add("privkey", new JsonPrimitive(Utils.bytesToHex(Utils.xor(sessionKey, privKeyBytes))));
|
||||
}
|
||||
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
public byte[] addAuth(String cmd, Map<String, Object> args, String cvc) throws CardException {
|
||||
if(cvc.length() < 6 || cvc.length() > 32) {
|
||||
throw new IllegalArgumentException("CVC cannot be of length " + cvc.length());
|
||||
}
|
||||
|
||||
if(lastCardNonce == null || cardPubkey == null) {
|
||||
getStatus();
|
||||
}
|
||||
|
||||
try {
|
||||
ECKey ephemeralKey = new ECKey(secureRandom);
|
||||
if(Secp256k1Context.isEnabled()) {
|
||||
byte[] sessionKey = NativeSecp256k1.createECDHSecret(ephemeralKey.getPrivKeyBytes(), cardPubkey);
|
||||
byte[] md = Sha256Hash.hash(Utils.concat(lastCardNonce, cmd.getBytes(StandardCharsets.UTF_8)));
|
||||
byte[] mask = Arrays.copyOf(Utils.xor(sessionKey, md), cvc.length());
|
||||
byte[] xcvc = Utils.xor(cvc.getBytes(StandardCharsets.UTF_8), mask);
|
||||
|
||||
args.put("epubkey", ephemeralKey.getPubKey());
|
||||
args.put("xcvc", xcvc);
|
||||
|
||||
if(cmd.equals("sign") && args.get("digest") instanceof byte[] digestBytes) {
|
||||
args.put("digest", Utils.xor(digestBytes, sessionKey));
|
||||
} else if(cmd.equals("change") && args.get("data") instanceof byte[] dataBytes) {
|
||||
args.put("data", Utils.xor(dataBytes, Arrays.copyOf(sessionKey, dataBytes.length)));
|
||||
}
|
||||
|
||||
return sessionKey;
|
||||
} else {
|
||||
throw new IllegalStateException("Native library libsecp256k1 required but not enabled");
|
||||
}
|
||||
} catch(NativeSecp256k1Util.AssertFailException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Sha256Hash getVerificationData(byte[] cardNonce, byte[] userNonce, byte[] commandData) {
|
||||
byte[] data = new byte[40 + commandData.length];
|
||||
System.arraycopy(OPENDIME_HEADER, 0, data, 0, 8);
|
||||
System.arraycopy(cardNonce, 0, data, 8, 16);
|
||||
System.arraycopy(userNonce, 0, data, 24, 16);
|
||||
System.arraycopy(commandData, 0, data, 40, commandData.length);
|
||||
return Sha256Hash.of(data);
|
||||
}
|
||||
|
||||
private byte[] getNonce() {
|
||||
byte[] nonce = new byte[16];
|
||||
secureRandom.nextBytes(nonce);
|
||||
return nonce;
|
||||
}
|
||||
|
||||
private static class ByteArrayToHexTypeAdapter implements JsonSerializer<byte[]>, JsonDeserializer<byte[]> {
|
||||
public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return Utils.hexToBytes(json.getAsString());
|
||||
}
|
||||
|
||||
public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(Utils.bytesToHex(src));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class CardRead extends CardResponse {
|
||||
byte[] sig;
|
||||
byte[] pubkey;
|
||||
|
||||
public ECDSASignature getSignature() {
|
||||
if(sig != null) {
|
||||
BigInteger r = new BigInteger(1, Arrays.copyOfRange(sig, 0, 32));
|
||||
BigInteger s = new BigInteger(1, Arrays.copyOfRange(sig, 32, 64));
|
||||
return new ECDSASignature(r, s);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ECKey getPubKey() {
|
||||
return ECKey.fromPublicOnly(pubkey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardResponse {
|
||||
byte[] card_nonce;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardSetup extends CardResponse {
|
||||
int slot;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardSign extends CardRead {
|
||||
int slot;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ECDSASignature;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class CardSignature extends CardResponse {
|
||||
byte[] auth_sig;
|
||||
|
||||
public ECDSASignature getSignature() {
|
||||
BigInteger r = new BigInteger(1, Arrays.copyOfRange(auth_sig, 0, 32));
|
||||
BigInteger s = new BigInteger(1, Arrays.copyOfRange(auth_sig, 32, 64));
|
||||
return new ECDSASignature(r, s);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CardStatus extends CardResponse {
|
||||
int proto;
|
||||
String ver;
|
||||
BigInteger birth;
|
||||
boolean tapsigner;
|
||||
List<BigInteger> path;
|
||||
BigInteger num_backups;
|
||||
List<BigInteger> slots;
|
||||
String addr;
|
||||
byte[] pubkey;
|
||||
BigInteger auth_delay;
|
||||
boolean testnet;
|
||||
|
||||
public boolean isInitialized() {
|
||||
return (getCardType() == WalletModel.TAPSIGNER && path != null) || (getCardType() == WalletModel.SATSCARD && addr != null);
|
||||
}
|
||||
|
||||
public String getIdentifier() {
|
||||
byte[] pubkeyHash = Sha256Hash.hash(pubkey);
|
||||
String base32 = BaseEncoding.base32().encode(Arrays.copyOfRange(pubkeyHash, 8, 32));
|
||||
return base32.replaceAll("(.{5})", "$1-");
|
||||
}
|
||||
|
||||
public List<ChildNumber> getDerivation() {
|
||||
if(isInitialized()) {
|
||||
return path.stream().map(i -> new ChildNumber(i.intValue())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean requiresBackup() {
|
||||
return num_backups == null || num_backups.intValue() == 0;
|
||||
}
|
||||
|
||||
public WalletModel getCardType() {
|
||||
return tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD;
|
||||
}
|
||||
|
||||
public int getCurrentSlot() {
|
||||
if(slots == null || slots.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return slots.get(0).intValue();
|
||||
}
|
||||
|
||||
public int getLastSlot() {
|
||||
if(slots == null || slots.size() < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return slots.get(1).intValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CardStatus{" +
|
||||
"proto=" + proto +
|
||||
", ver='" + ver + '\'' +
|
||||
", birth=" + birth +
|
||||
", tapsigner=" + tapsigner +
|
||||
", path=" + path +
|
||||
", num_backups=" + num_backups +
|
||||
", slots=" + slots +
|
||||
", addr='" + addr + '\'' +
|
||||
", pubkey=" + Arrays.toString(pubkey) +
|
||||
", auth_delay=" + auth_delay +
|
||||
", testnet=" + testnet +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import co.nstant.in.cbor.CborBuilder;
|
||||
import co.nstant.in.cbor.CborDecoder;
|
||||
import co.nstant.in.cbor.CborEncoder;
|
||||
import co.nstant.in.cbor.CborException;
|
||||
import co.nstant.in.cbor.builder.ArrayBuilder;
|
||||
import co.nstant.in.cbor.builder.MapBuilder;
|
||||
import co.nstant.in.cbor.model.*;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
|
||||
import com.sparrowwallet.sparrow.io.CardUnluckyNumberException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.*;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class CardTransport {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardTransport.class);
|
||||
|
||||
public static final String APPID = "f0436f696e6b697465434152447631";
|
||||
private static final int CBOR_CLA = 0x00;
|
||||
private static final int CBOR_INS = 0xCB;
|
||||
private static final int SW_OKAY = 0x9000;
|
||||
|
||||
private final Card connection;
|
||||
|
||||
CardTransport() throws CardException {
|
||||
TerminalFactory tf = TerminalFactory.getDefault();
|
||||
List<CardTerminal> terminals = tf.terminals().list();
|
||||
if(terminals.isEmpty()) {
|
||||
throw new IllegalStateException("No reader connected");
|
||||
}
|
||||
|
||||
CardTerminal cardTerminal = (CardTerminal)terminals.get(0);
|
||||
connection = cardTerminal.connect("*");
|
||||
|
||||
CardChannel cardChannel = connection.getBasicChannel();
|
||||
ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(0, 0xA4, 4, 0, Utils.hexToBytes(APPID.toUpperCase())));
|
||||
if(resp.getSW() != SW_OKAY) {
|
||||
log.error("Card initialization error, response was 0x" + Integer.toHexString(resp.getSW()));
|
||||
throw new CardException("Card initialization error, response was 0x" + Integer.toHexString(resp.getSW()) + ". Note that only the Tapsigner is currently supported.");
|
||||
}
|
||||
}
|
||||
|
||||
JsonObject send(String cmd, Map<String, Object> args) throws CardException {
|
||||
Map<String, Object> sendMap = new LinkedHashMap<>();
|
||||
sendMap.put("cmd", cmd);
|
||||
sendMap.putAll(args);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
MapBuilder<CborBuilder> cborBuilder = new CborBuilder().addMap();
|
||||
for(Map.Entry<String, Object> entry : sendMap.entrySet()) {
|
||||
if(entry.getValue() instanceof String strValue) {
|
||||
cborBuilder.put(entry.getKey(), strValue);
|
||||
} else if(entry.getValue() instanceof byte[] byteValue) {
|
||||
cborBuilder.put(entry.getKey(), byteValue);
|
||||
} else if(entry.getValue() instanceof Long longValue) {
|
||||
cborBuilder.put(entry.getKey(), longValue);
|
||||
} else if(entry.getValue() instanceof Integer integerValue) {
|
||||
cborBuilder.put(entry.getKey(), integerValue);
|
||||
} else if(entry.getValue() instanceof Boolean booleanValue) {
|
||||
cborBuilder.put(entry.getKey(), booleanValue);
|
||||
} else if(entry.getValue() instanceof List<?> listValue) {
|
||||
ArrayBuilder<MapBuilder<CborBuilder>> arrayBuilder = cborBuilder.putArray(entry.getKey());
|
||||
for(Object value : listValue) {
|
||||
if(value instanceof String strValue) {
|
||||
arrayBuilder.add(strValue);
|
||||
} else if(value instanceof byte[] byteValue) {
|
||||
arrayBuilder.add(byteValue);
|
||||
} else if(value instanceof Long longValue) {
|
||||
arrayBuilder.add(longValue);
|
||||
} else if(value instanceof Boolean booleanValue) {
|
||||
arrayBuilder.add(booleanValue);
|
||||
}
|
||||
}
|
||||
arrayBuilder.end();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
new CborEncoder(baos).encode(cborBuilder.end().build());
|
||||
byte[] sendBytes = baos.toByteArray();
|
||||
|
||||
CardChannel cardChannel = connection.getBasicChannel();
|
||||
ResponseAPDU resp = cardChannel.transmit(new CommandAPDU(CBOR_CLA, CBOR_INS, 0, 0, sendBytes));
|
||||
|
||||
if(resp.getSW() != SW_OKAY) {
|
||||
throw new CardException("Received error SW value " + resp.getSW());
|
||||
}
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(resp.getData());
|
||||
List<DataItem> dataItems = new CborDecoder(bais).decode();
|
||||
for(DataItem dataItem : dataItems) {
|
||||
if(dataItem instanceof co.nstant.in.cbor.model.Map map) {
|
||||
JsonObject result = new JsonObject();
|
||||
for(DataItem key : map.getKeys()) {
|
||||
String strKey = key.toString();
|
||||
result.add(strKey, getJsonElement(map.get(key)));
|
||||
}
|
||||
|
||||
if(result.get("error") != null) {
|
||||
String msg = result.get("error").getAsString();
|
||||
int code = result.get("code") == null ? 500 : result.get("code").getAsInt();
|
||||
if(code == 205) {
|
||||
throw new CardUnluckyNumberException("Card chose unlucky number, please retry");
|
||||
} else if(code == 401) {
|
||||
throw new CardAuthorizationException("Incorrect PIN provided");
|
||||
}
|
||||
|
||||
throw new CardException(code + " on " + cmd + ": " + msg);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} catch(CborException e) {
|
||||
log.error("CBOR encoding error", e);
|
||||
}
|
||||
|
||||
return new JsonObject();
|
||||
}
|
||||
|
||||
private JsonElement getJsonElement(DataItem dataItem) {
|
||||
if(dataItem instanceof UnicodeString strValue) {
|
||||
return new JsonPrimitive(strValue.toString());
|
||||
} else if(dataItem instanceof ByteString byteString) {
|
||||
return new JsonPrimitive(Utils.bytesToHex(byteString.getBytes()));
|
||||
} else if(dataItem instanceof UnsignedInteger unsignedInteger) {
|
||||
return new JsonPrimitive(unsignedInteger.getValue());
|
||||
} else if(dataItem instanceof SimpleValue simpleValue) {
|
||||
return new JsonPrimitive(simpleValue.getValue() == SimpleValueType.TRUE.getValue());
|
||||
} else if(dataItem instanceof Array array) {
|
||||
JsonArray jsonArray = new JsonArray();
|
||||
for(DataItem item : array.getDataItems()) {
|
||||
jsonArray.add(getJsonElement(item));
|
||||
}
|
||||
return jsonArray;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Cannot convert dataItem of type " + dataItem.getClass() + "to JsonElement");
|
||||
}
|
||||
|
||||
void disconnect() throws CardException {
|
||||
connection.disconnect(true);
|
||||
}
|
||||
|
||||
static boolean isReaderAvailable() {
|
||||
try {
|
||||
TerminalFactory tf = TerminalFactory.getDefault();
|
||||
return !tf.terminals().list().isEmpty();
|
||||
} catch(Exception e) {
|
||||
log.error("Error detecting card terminals", e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
|
||||
public class CardUnseal extends CardResponse {
|
||||
int slot;
|
||||
byte[] privkey;
|
||||
byte[] pubkey;
|
||||
byte[] master_pk;
|
||||
byte[] chain_code;
|
||||
|
||||
public ECKey getPrivateKey() {
|
||||
return ECKey.fromPrivate(privkey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
public class CardWait {
|
||||
boolean success;
|
||||
BigInteger auth_delay;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
public class CardXpub {
|
||||
byte[] xpub;
|
||||
}
|
||||
@@ -0,0 +1,531 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInputSigner;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.control.CardImportPane;
|
||||
import com.sparrowwallet.sparrow.io.CardApi;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class CkCardApi extends CardApi {
|
||||
private static final Logger log = LoggerFactory.getLogger(CkCardApi.class);
|
||||
|
||||
private final WalletModel cardType;
|
||||
private final CardProtocol cardProtocol;
|
||||
private String cvc;
|
||||
|
||||
public CkCardApi(String cvc) throws CardException {
|
||||
this(WalletModel.TAPSIGNER, cvc);
|
||||
}
|
||||
|
||||
public CkCardApi(WalletModel cardType, String cvc) throws CardException {
|
||||
this.cardType = cardType;
|
||||
this.cardProtocol = new CardProtocol();
|
||||
this.cvc = cvc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInitialized() throws CardException {
|
||||
CardStatus cardStatus = getStatus();
|
||||
return cardStatus.isInitialized();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(int slot, byte[] chainCode) throws CardException {
|
||||
cardProtocol.verify();
|
||||
cardProtocol.setup(cvc, slot, chainCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getCardType() throws CardException {
|
||||
CardStatus cardStatus = getStatus();
|
||||
return cardStatus.getCardType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCurrentSlot() throws CardException {
|
||||
CardStatus cardStatus = getStatus();
|
||||
return cardStatus.getCurrentSlot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType getDefaultScriptType() {
|
||||
return ScriptType.P2WPKH;
|
||||
}
|
||||
|
||||
CardStatus getStatus() throws CardException {
|
||||
CardStatus cardStatus = cardProtocol.getStatus();
|
||||
if(cardType != null && cardStatus.getCardType() != cardType) {
|
||||
throw new CardException("Please use a " + cardType.toDisplayString() + " card.");
|
||||
}
|
||||
return cardStatus;
|
||||
}
|
||||
|
||||
void checkWait(CardStatus cardStatus, IntegerProperty delayProperty, StringProperty messageProperty) throws CardException {
|
||||
if(cardStatus.auth_delay != null) {
|
||||
int delay = cardStatus.auth_delay.intValue();
|
||||
while(delay > 0) {
|
||||
delayProperty.set(delay);
|
||||
messageProperty.set("Auth delay, waiting " + delay + "s...");
|
||||
CardWait cardWait = cardProtocol.authWait();
|
||||
if(cardWait.success) {
|
||||
delay = cardWait.auth_delay == null ? 0 : cardWait.auth_delay.intValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<Void> getAuthDelayService() throws CardException {
|
||||
CardStatus cardStatus = getStatus();
|
||||
if(cardStatus.auth_delay != null) {
|
||||
return new AuthDelayService(cardStatus);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresBackup() throws CardException {
|
||||
CardStatus cardStatus = getStatus();
|
||||
return cardStatus.requiresBackup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<String> getBackupService() {
|
||||
return new BackupService();
|
||||
}
|
||||
|
||||
String getBackup() throws CardException {
|
||||
CardBackup cardBackup = cardProtocol.backup(cvc);
|
||||
return Base64.getEncoder().encodeToString(cardBackup.data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean changePin(String newCvc) throws CardException {
|
||||
CardChange cardChange = cardProtocol.change(cvc, newCvc);
|
||||
if(cardChange.success) {
|
||||
cvc = newCvc;
|
||||
}
|
||||
|
||||
return cardChange.success;
|
||||
}
|
||||
|
||||
void setDerivation(List<ChildNumber> derivation) throws CardException {
|
||||
cardProtocol.derive(cvc, derivation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<Void> getInitializationService(byte[] entropy, StringProperty messageProperty) {
|
||||
if(cardType == WalletModel.TAPSIGNER) {
|
||||
return new CardImportPane.CardInitializationService(new Tapsigner(), cvc, entropy, messageProperty);
|
||||
}
|
||||
|
||||
return new CardInitializationService(entropy, messageProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
return new CardImportPane.CardImportService(new Tapsigner(), cvc, derivation, messageProperty);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore() throws CardException {
|
||||
KeyDerivation keyDerivation = getKeyDerivation();
|
||||
|
||||
CardXpub derivedXpub = cardProtocol.xpub(cvc, false);
|
||||
ExtendedKey derivedXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(derivedXpub.xpub));
|
||||
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(WalletModel.TAPSIGNER.toDisplayString());
|
||||
keystore.setKeyDerivation(keyDerivation);
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setExtendedPublicKey(derivedXpubkey);
|
||||
keystore.setWalletModel(WalletModel.TAPSIGNER);
|
||||
|
||||
return keystore;
|
||||
}
|
||||
|
||||
private KeyDerivation getKeyDerivation() throws CardException {
|
||||
String masterFingerprint = getMasterFingerprint();
|
||||
return new KeyDerivation(masterFingerprint, getStatus().getDerivation());
|
||||
}
|
||||
|
||||
private String getMasterFingerprint() throws CardException {
|
||||
CardXpub masterXpub = cardProtocol.xpub(cvc, true);
|
||||
ExtendedKey masterXpubkey = ExtendedKey.fromDescriptor(Base58.encodeChecked(masterXpub.xpub));
|
||||
return Utils.bytesToHex(masterXpubkey.getKey().getFingerprint());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<PSBT> getSignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) {
|
||||
return new SignService(wallet, psbt, messageProperty);
|
||||
}
|
||||
|
||||
void sign(Wallet wallet, PSBT psbt) throws CardException {
|
||||
Keystore cardKeystore = getKeystore();
|
||||
KeyDerivation cardKeyDerivation = cardKeystore.getKeyDerivation();
|
||||
|
||||
Map<PSBTInput, WalletNode> signingNodes = wallet.getSigningNodes(psbt);
|
||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||
if(!psbtInput.isSigned()) {
|
||||
WalletNode signingNode = signingNodes.get(psbtInput);
|
||||
KeyDerivation changedDerivation = null;
|
||||
try {
|
||||
ECKey cardSigningPubKey = cardKeystore.getPubKey(signingNode);
|
||||
if(wallet.getKeystores().stream().noneMatch(keystore -> keystore.getPubKey(signingNode).equals(cardSigningPubKey))) {
|
||||
Optional<KeyDerivation> optKeyDerivation = wallet.getKeystores().stream().map(Keystore::getKeyDerivation)
|
||||
.filter(kd -> kd.getMasterFingerprint().equals(cardKeyDerivation.getMasterFingerprint()) && !kd.getDerivation().equals(cardKeyDerivation.getDerivation())).findFirst();
|
||||
if(optKeyDerivation.isPresent()) {
|
||||
changedDerivation = optKeyDerivation.get();
|
||||
setDerivation(changedDerivation.getDerivation());
|
||||
|
||||
Keystore changedKeystore = getKeystore();
|
||||
ECKey changedSigningPubKey = changedKeystore.getPubKey(signingNode);
|
||||
if(wallet.getKeystores().stream().noneMatch(keystore -> keystore.getPubKey(signingNode).equals(changedSigningPubKey))) {
|
||||
throw new CardException("Card cannot recognise public key for signing address.");
|
||||
}
|
||||
} else {
|
||||
throw new CardException("Card cannot recognise public key for signing address.");
|
||||
}
|
||||
}
|
||||
|
||||
psbtInput.sign(new CardPSBTInputSigner(signingNode));
|
||||
} finally {
|
||||
if(changedDerivation != null) {
|
||||
setDerivation(cardKeyDerivation.getDerivation());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<String> getSignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
return new SignMessageService(message, scriptType, derivation, messageProperty);
|
||||
}
|
||||
|
||||
String signMessage(String message, ScriptType scriptType, List<ChildNumber> derivation) throws CardException {
|
||||
List<ChildNumber> keystoreDerivation = derivation.subList(0, derivation.size() - 2);
|
||||
List<ChildNumber> subPathDerivation = derivation.subList(derivation.size() - 2, derivation.size());
|
||||
|
||||
Keystore cardKeystore = getKeystore();
|
||||
KeyDerivation cardKeyDerivation = cardKeystore.getKeyDerivation();
|
||||
Keystore signingKeystore = cardKeystore;
|
||||
try {
|
||||
if(!cardKeyDerivation.getDerivation().equals(keystoreDerivation)) {
|
||||
setDerivation(keystoreDerivation);
|
||||
signingKeystore = getKeystore();
|
||||
}
|
||||
|
||||
WalletNode addressNode = new WalletNode(KeyDerivation.writePath(subPathDerivation));
|
||||
ECKey addressPubKey = signingKeystore.getPubKey(addressNode);
|
||||
return addressPubKey.signMessage(message, scriptType, hash -> {
|
||||
try {
|
||||
CardSign cardSign = cardProtocol.sign(cvc, subPathDerivation, hash);
|
||||
return cardSign.getSignature();
|
||||
} catch(CardException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
if(signingKeystore != cardKeystore) {
|
||||
setDerivation(cardKeystore.getKeyDerivation().getDerivation());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<ECKey> getPrivateKeyService(Integer slot, StringProperty messageProperty) {
|
||||
return new PrivateKeyService(slot, messageProperty);
|
||||
}
|
||||
|
||||
ECKey getPrivateKey(int slot, int currentSlot) throws CardException {
|
||||
if(slot != currentSlot) {
|
||||
CardAuthDump cardAuthDump = cardProtocol.dump(cvc, slot);
|
||||
return cardAuthDump.getPrivateKey();
|
||||
}
|
||||
|
||||
CardUnseal cardUnseal = cardProtocol.unseal(cvc, slot);
|
||||
return cardUnseal.getPrivateKey();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Service<Address> getAddressService(StringProperty messageProperty) {
|
||||
return new AddressService(messageProperty);
|
||||
}
|
||||
|
||||
Address getAddress(int currentSlot, int lastSlot, String addr) throws CardException {
|
||||
if(currentSlot == lastSlot) {
|
||||
CardDump cardDump = cardProtocol.dump(currentSlot);
|
||||
if(!cardDump.sealed) {
|
||||
return cardDump.getAddress();
|
||||
}
|
||||
}
|
||||
|
||||
CardRead cardRead = cardProtocol.read(null, currentSlot);
|
||||
Address address = getDefaultScriptType().getAddress(cardRead.getPubKey());
|
||||
|
||||
String left = addr.substring(0, addr.indexOf('_'));
|
||||
String right = addr.substring(addr.lastIndexOf('_') + 1);
|
||||
|
||||
if(!address.toString().startsWith(left) || !address.toString().endsWith(right)) {
|
||||
throw new CardException("Card authentication failed: Provided pubkey does not match given address");
|
||||
}
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disconnect() {
|
||||
try {
|
||||
cardProtocol.disconnect();
|
||||
} catch(CardException e) {
|
||||
log.warn("Error disconnecting from card reader", e);
|
||||
}
|
||||
}
|
||||
|
||||
public class CardInitializationService extends Service<Void> {
|
||||
private final byte[] chainCode;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public CardInitializationService(byte[] chainCode, StringProperty messageProperty) {
|
||||
this.chainCode = chainCode;
|
||||
this.messageProperty = messageProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
CardStatus cardStatus = getStatus();
|
||||
if(cardStatus.getCardType() != WalletModel.SATSCARD) {
|
||||
throw new IllegalStateException("Please use a " + WalletModel.SATSCARD.toDisplayString() + ".");
|
||||
}
|
||||
if(cardStatus.isInitialized()) {
|
||||
throw new IllegalStateException("Card already initialized.");
|
||||
}
|
||||
|
||||
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
|
||||
|
||||
initialize(cardStatus.getCurrentSlot(), chainCode);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class AuthDelayService extends Service<Void> {
|
||||
private final CardStatus cardStatus;
|
||||
private final IntegerProperty delayProperty;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
AuthDelayService(CardStatus cardStatus) {
|
||||
this.cardStatus = cardStatus;
|
||||
this.delayProperty = new SimpleIntegerProperty();
|
||||
this.messageProperty = new SimpleStringProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
delayProperty.addListener((observable, oldValue, newValue) -> updateProgress(cardStatus.auth_delay.intValue() - newValue.intValue(), cardStatus.auth_delay.intValue()));
|
||||
messageProperty.addListener((observable, oldValue, newValue) -> updateMessage(newValue));
|
||||
checkWait(cardStatus, delayProperty, messageProperty);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class BackupService extends Service<String> {
|
||||
@Override
|
||||
protected Task<String> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected String call() throws Exception {
|
||||
return getBackup();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class SignService extends Service<PSBT> {
|
||||
private final Wallet wallet;
|
||||
private final PSBT psbt;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public SignService(Wallet wallet, PSBT psbt, StringProperty messageProperty) {
|
||||
this.wallet = wallet;
|
||||
this.psbt = psbt;
|
||||
this.messageProperty = messageProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<PSBT> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected PSBT call() throws Exception {
|
||||
CardStatus cardStatus = getStatus();
|
||||
if(cardStatus.getCardType() != WalletModel.TAPSIGNER) {
|
||||
throw new IllegalStateException("Please use a " + WalletModel.TAPSIGNER.toDisplayString() + " to sign transactions.");
|
||||
}
|
||||
|
||||
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
|
||||
|
||||
sign(wallet, psbt);
|
||||
return psbt;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private class CardPSBTInputSigner implements PSBTInputSigner {
|
||||
private final WalletNode signingNode;
|
||||
private ECKey pubkey;
|
||||
|
||||
public CardPSBTInputSigner(WalletNode signingNode) {
|
||||
this.signingNode = signingNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionSignature sign(Sha256Hash hash, SigHash sigHash, TransactionSignature.Type signatureType) {
|
||||
if(signatureType != TransactionSignature.Type.ECDSA) {
|
||||
throw new IllegalStateException(WalletModel.TAPSIGNER.toDisplayString() + " cannot sign " + signatureType + " transactions.");
|
||||
}
|
||||
|
||||
try {
|
||||
CardSign cardSign = cardProtocol.sign(cvc, signingNode.getDerivation(), hash);
|
||||
pubkey = cardSign.getPubKey();
|
||||
return new TransactionSignature(cardSign.getSignature(), sigHash);
|
||||
} catch(CardException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ECKey getPubKey() {
|
||||
return pubkey;
|
||||
}
|
||||
}
|
||||
|
||||
public class SignMessageService extends Service<String> {
|
||||
private final String message;
|
||||
private final ScriptType scriptType;
|
||||
private final List<ChildNumber> derivation;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public SignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
this.message = message;
|
||||
this.scriptType = scriptType;
|
||||
this.derivation = derivation;
|
||||
this.messageProperty = messageProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<String> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected String call() throws Exception {
|
||||
CardStatus cardStatus = getStatus();
|
||||
if(cardStatus.getCardType() != WalletModel.TAPSIGNER) {
|
||||
throw new IllegalStateException("Please use a " + WalletModel.TAPSIGNER.toDisplayString() + " to sign messages.");
|
||||
}
|
||||
|
||||
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
|
||||
|
||||
return signMessage(message, scriptType, derivation);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class PrivateKeyService extends Service<ECKey> {
|
||||
private Integer slot;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public PrivateKeyService(Integer slot, StringProperty messageProperty) {
|
||||
this.slot = slot;
|
||||
this.messageProperty = messageProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<ECKey> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected ECKey call() throws Exception {
|
||||
CardStatus cardStatus = getStatus();
|
||||
if(cardStatus.getCardType() != WalletModel.SATSCARD) {
|
||||
throw new IllegalStateException("Please use a " + WalletModel.SATSCARD.toDisplayString() + " to retrieve a private key.");
|
||||
}
|
||||
|
||||
int currentSlot = cardStatus.getCurrentSlot();
|
||||
if(slot == null) {
|
||||
slot = currentSlot;
|
||||
}
|
||||
|
||||
if(slot == currentSlot && !cardStatus.isInitialized()) {
|
||||
//If card has been unsealed, but a new slot is not initialized, retrieve private key for previous slot
|
||||
slot = slot - 1;
|
||||
}
|
||||
|
||||
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
|
||||
|
||||
return getPrivateKey(slot, currentSlot);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class AddressService extends Service<Address> {
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public AddressService(StringProperty messageProperty) {
|
||||
this.messageProperty = messageProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Address> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Address call() throws Exception {
|
||||
CardStatus cardStatus = getStatus();
|
||||
if(cardStatus.getCardType() != WalletModel.SATSCARD) {
|
||||
throw new IllegalStateException("Please use a " + WalletModel.SATSCARD.toDisplayString() + " to retrieve an address.");
|
||||
}
|
||||
if(!cardStatus.isInitialized()) {
|
||||
throw new IllegalStateException("Please re-initialize card before attempting to get the address.");
|
||||
}
|
||||
|
||||
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
|
||||
|
||||
return getAddress(cardStatus.getCurrentSlot(), cardStatus.getLastSlot(), cardStatus.addr);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.sparrowwallet.sparrow.io.ckcard;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreCardImport;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import java.util.List;
|
||||
|
||||
public class Tapsigner implements KeystoreCardImport {
|
||||
@Override
|
||||
public boolean isInitialized() throws CardException {
|
||||
CkCardApi cardApi = null;
|
||||
try {
|
||||
cardApi = new CkCardApi(null);
|
||||
return cardApi.isInitialized();
|
||||
} finally {
|
||||
if(cardApi != null) {
|
||||
cardApi.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(String pin, byte[] chainCode, StringProperty messageProperty) throws CardException {
|
||||
if(pin.length() < 6) {
|
||||
throw new CardException("PIN too short.");
|
||||
}
|
||||
|
||||
if(pin.length() > 32) {
|
||||
throw new CardException("PIN too long.");
|
||||
}
|
||||
|
||||
CkCardApi cardApi = null;
|
||||
try {
|
||||
cardApi = new CkCardApi(pin);
|
||||
CardStatus cardStatus = cardApi.getStatus();
|
||||
if(cardStatus.isInitialized()) {
|
||||
throw new IllegalStateException("Card is already initialized.");
|
||||
}
|
||||
cardApi.checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
|
||||
cardApi.initialize(0, chainCode);
|
||||
} finally {
|
||||
if(cardApi != null) {
|
||||
cardApi.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(String pin, List<ChildNumber> derivation, StringProperty messageProperty) throws ImportException {
|
||||
if(pin.length() < 6) {
|
||||
throw new ImportException("PIN too short.");
|
||||
}
|
||||
|
||||
if(pin.length() > 32) {
|
||||
throw new ImportException("PIN too long.");
|
||||
}
|
||||
|
||||
CkCardApi cardApi = null;
|
||||
try {
|
||||
cardApi = new CkCardApi(pin);
|
||||
CardStatus cardStatus = cardApi.getStatus();
|
||||
if(!cardStatus.isInitialized()) {
|
||||
throw new IllegalStateException("Card is not initialized.");
|
||||
}
|
||||
cardApi.checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
|
||||
|
||||
if(!derivation.equals(cardStatus.getDerivation())) {
|
||||
cardApi.setDerivation(derivation);
|
||||
}
|
||||
return cardApi.getKeystore();
|
||||
} catch(Exception e) {
|
||||
throw new ImportException(e);
|
||||
} finally {
|
||||
if(cardApi != null) {
|
||||
cardApi.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import the keystore from your Tapsigner by placing it on the card reader.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Tapsigner";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.TAPSIGNER;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.io.Files;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.Argon2KeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver;
|
||||
@@ -31,6 +30,7 @@ import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
@@ -245,7 +245,7 @@ public class DbPersistence implements Persistence {
|
||||
walletNodeDao.updateNodeAddressData(addressNode.getId(), addressNode.getAddressData());
|
||||
}
|
||||
|
||||
Set<BlockTransactionHashIndex> txos = addressNode.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo)).collect(Collectors.toSet());
|
||||
List<BlockTransactionHashIndex> txos = addressNode.getTransactionOutputs().stream().flatMap(txo -> txo.isSpent() ? Stream.of(txo, txo.getSpentBy()) : Stream.of(txo)).collect(Collectors.toList());
|
||||
List<Long> existingIds = txos.stream().map(Persistable::getId).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
referencedTxIds.addAll(txos.stream().map(BlockTransactionHash::getHash).collect(Collectors.toSet()));
|
||||
|
||||
@@ -315,6 +315,11 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
if(dirtyPersistables.walletConfig) {
|
||||
WalletConfigDao walletConfigDao = handle.attach(WalletConfigDao.class);
|
||||
walletConfigDao.addOrUpdate(wallet, wallet.getWalletConfig());
|
||||
}
|
||||
|
||||
if(dirtyPersistables.mixConfig) {
|
||||
MixConfigDao mixConfigDao = handle.attach(MixConfigDao.class);
|
||||
mixConfigDao.addOrUpdate(wallet, wallet.getMixConfig());
|
||||
@@ -620,25 +625,26 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
|
||||
//Flyway does not support JPMS yet, so the migration files are extracted to a temp dir in order to avoid classloader encapsulation issues
|
||||
private File getMigrationDir() {
|
||||
File migrationDir = Files.createTempDir();
|
||||
private File getMigrationDir() throws StorageException {
|
||||
try {
|
||||
File migrationDir = Files.createTempDirectory("sparrowfly").toFile();
|
||||
String[] files = IOUtils.getResourceListing(DbPersistence.class, MIGRATION_RESOURCES_DIR);
|
||||
for(String name : files) {
|
||||
File targetFile = new File(migrationDir, name);
|
||||
try(InputStream inputStream = DbPersistence.class.getResourceAsStream("/" + MIGRATION_RESOURCES_DIR + name)) {
|
||||
if(inputStream != null) {
|
||||
java.nio.file.Files.copy(inputStream, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.copy(inputStream, targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||
} else {
|
||||
log.error("Could not load resource at /" + MIGRATION_RESOURCES_DIR + name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return migrationDir;
|
||||
} catch(Exception e) {
|
||||
log.error("Could not extract migration resources", e);
|
||||
throw new StorageException("Could not extract migration resources", e);
|
||||
}
|
||||
|
||||
return migrationDir;
|
||||
}
|
||||
|
||||
private HikariDataSource getDataSource(Storage storage, String password) throws StorageException {
|
||||
@@ -745,6 +751,13 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletConfigChanged(WalletConfigChangedEvent event) {
|
||||
if(persistsFor(event.getWallet()) && event.getWallet().getWalletConfig() != null) {
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).walletConfig = true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletMixConfigChanged(WalletMixConfigChangedEvent event) {
|
||||
if(persistsFor(event.getWallet()) && event.getWallet().getMixConfig() != null) {
|
||||
@@ -793,6 +806,7 @@ public class DbPersistence implements Persistence {
|
||||
public Integer watchLast = null;
|
||||
public final List<Entry> labelEntries = new ArrayList<>();
|
||||
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>();
|
||||
public boolean walletConfig;
|
||||
public boolean mixConfig;
|
||||
public final Map<Sha256Hash, UtxoMixData> changedUtxoMixes = new HashMap<>();
|
||||
public final Map<Sha256Hash, UtxoMixData> removedUtxoMixes = new HashMap<>();
|
||||
@@ -812,6 +826,7 @@ public class DbPersistence implements Persistence {
|
||||
"\nAddress labels:" + labelEntries.stream().filter(entry -> entry instanceof NodeEntry).map(entry -> ((NodeEntry)entry).getNode().toString() + " " + entry.getLabel()).collect(Collectors.toList()) +
|
||||
"\nUTXO labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) +
|
||||
"\nUTXO statuses:" + utxoStatuses +
|
||||
"\nWallet config:" + walletConfig +
|
||||
"\nMix config:" + mixConfig +
|
||||
"\nUTXO mixes changed:" + changedUtxoMixes +
|
||||
"\nUTXO mixes removed:" + removedUtxoMixes +
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletConfig;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
|
||||
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
|
||||
public interface WalletConfigDao {
|
||||
@SqlQuery("select id, iconData, userIcon, usePayNym from walletConfig where wallet = ?")
|
||||
@RegisterRowMapper(WalletConfigMapper.class)
|
||||
WalletConfig getForWalletId(Long id);
|
||||
|
||||
@SqlUpdate("insert into walletConfig (iconData, userIcon, usePayNym, wallet) values (?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insertWalletConfig(byte[] iconData, boolean userIcon, boolean usePayNym, long wallet);
|
||||
|
||||
@SqlUpdate("update walletConfig set iconData = ?, userIcon = ?, usePayNym = ?, wallet = ? where id = ?")
|
||||
void updateWalletConfig(byte[] iconData, boolean userIcon, boolean usePayNym, long wallet, long id);
|
||||
|
||||
default void addWalletConfig(Wallet wallet) {
|
||||
if(wallet.getWalletConfig() != null) {
|
||||
addOrUpdate(wallet, wallet.getWalletConfig());
|
||||
}
|
||||
}
|
||||
|
||||
default void addOrUpdate(Wallet wallet, WalletConfig walletConfig) {
|
||||
if(walletConfig.getId() == null) {
|
||||
long id = insertWalletConfig(walletConfig.getIconData(), walletConfig.isUserIcon(), walletConfig.isUsePayNym(), wallet.getId());
|
||||
walletConfig.setId(id);
|
||||
} else {
|
||||
updateWalletConfig(walletConfig.getIconData(), walletConfig.isUserIcon(), walletConfig.isUsePayNym(), wallet.getId(), walletConfig.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletConfig;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class WalletConfigMapper implements RowMapper<WalletConfig> {
|
||||
@Override
|
||||
public WalletConfig map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
byte[] iconData = rs.getBytes("iconData");
|
||||
boolean userIcon = rs.getBoolean("userIcon");
|
||||
boolean usePayNym = rs.getBoolean("usePayNym");
|
||||
|
||||
WalletConfig walletConfig = new WalletConfig(iconData, userIcon, usePayNym);
|
||||
walletConfig.setId(rs.getLong("id"));
|
||||
return walletConfig;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.UtxoMixData;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.jdbi.v3.sqlobject.CreateSqlObject;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
|
||||
import org.jdbi.v3.sqlobject.customizer.Bind;
|
||||
@@ -33,6 +30,9 @@ public interface WalletDao {
|
||||
@CreateSqlObject
|
||||
DetachedLabelDao createDetachedLabelDao();
|
||||
|
||||
@CreateSqlObject
|
||||
WalletConfigDao createWalletConfigDao();
|
||||
|
||||
@CreateSqlObject
|
||||
MixConfigDao createMixConfigDao();
|
||||
|
||||
@@ -118,6 +118,7 @@ public interface WalletDao {
|
||||
Map<String, String> detachedLabels = createDetachedLabelDao().getAll();
|
||||
wallet.getDetachedLabels().putAll(detachedLabels);
|
||||
|
||||
wallet.setWalletConfig(createWalletConfigDao().getForWalletId(wallet.getId()));
|
||||
wallet.setMixConfig(createMixConfigDao().getForWalletId(wallet.getId()));
|
||||
|
||||
Map<Sha256Hash, UtxoMixData> utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId());
|
||||
@@ -136,6 +137,7 @@ public interface WalletDao {
|
||||
createWalletNodeDao().addWalletNodes(wallet);
|
||||
createBlockTransactionDao().addBlockTransactions(wallet);
|
||||
createDetachedLabelDao().clearAndAddAll(wallet);
|
||||
createWalletConfigDao().addWalletConfig(wallet);
|
||||
createMixConfigDao().addMixConfig(wallet);
|
||||
createUtxoMixDataDao().addUtxoMixData(wallet);
|
||||
} finally {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import org.jdbi.v3.core.result.LinkedHashMapRowReducer;
|
||||
@@ -8,6 +9,8 @@ import org.jdbi.v3.core.result.RowView;
|
||||
import java.util.Map;
|
||||
|
||||
public class WalletNodeReducer implements LinkedHashMapRowReducer<Long, WalletNode> {
|
||||
private static final BlockTransactionHashIndex INPUT_MARKER = new BlockTransactionHashIndex(Sha256Hash.ZERO_HASH, 0, null, null, 0, 0);
|
||||
|
||||
@Override
|
||||
public void accumulate(Map<Long, WalletNode> map, RowView rowView) {
|
||||
WalletNode walletNode = map.computeIfAbsent(rowView.getColumn("walletNode.id", Long.class), id -> rowView.getRow(WalletNode.class));
|
||||
@@ -20,11 +23,18 @@ public class WalletNodeReducer implements LinkedHashMapRowReducer<Long, WalletNo
|
||||
if(rowView.getColumn("blockTransactionHashIndex.node", Long.class) != null) {
|
||||
BlockTransactionHashIndex blockTransactionHashIndex = rowView.getRow(BlockTransactionHashIndex.class);
|
||||
if(rowView.getColumn("blockTransactionHashIndex.spentBy", Long.class) != null) {
|
||||
BlockTransactionHashIndex spentBy = walletNode.getTransactionOutputs().stream().filter(ref -> ref.getId().equals(rowView.getColumn("blockTransactionHashIndex.spentBy", Long.class))).findFirst().orElseThrow();
|
||||
BlockTransactionHashIndex spentBy = walletNode.getTransactionOutputs().stream().filter(ref -> ref.getId().equals(rowView.getColumn("blockTransactionHashIndex.spentBy", Long.class))).findFirst()
|
||||
.orElseThrow(() -> new IllegalStateException("Cannot find transaction output for " + rowView.getColumn("blockTransactionHashIndex.spentBy", Long.class)));
|
||||
blockTransactionHashIndex.setSpentBy(spentBy);
|
||||
walletNode.getTransactionOutputs().remove(spentBy);
|
||||
spentBy.setSpentBy(null);
|
||||
}
|
||||
if(!walletNode.getTransactionOutputs().add(blockTransactionHashIndex)) {
|
||||
//Can only happen if we add an input with the same hash, index, height etc as an existing output
|
||||
//Set a marker we will clear when adding the output
|
||||
blockTransactionHashIndex.setSpentBy(INPUT_MARKER);
|
||||
walletNode.getTransactionOutputs().add(blockTransactionHashIndex);
|
||||
}
|
||||
walletNode.getTransactionOutputs().add(blockTransactionHashIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
package com.sparrowwallet.sparrow.keystoreimport;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.sparrow.control.CardImportPane;
|
||||
import com.sparrowwallet.sparrow.control.FileKeystoreImportPane;
|
||||
import com.sparrowwallet.sparrow.control.TitledDescriptionPane;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.io.ckcard.Tapsigner;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Accordion;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import static com.sparrowwallet.sparrow.io.CardApi.isReaderAvailable;
|
||||
|
||||
public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
private static final Logger log = LoggerFactory.getLogger(HwAirgappedController.class);
|
||||
|
||||
@FXML
|
||||
private Accordion importAccordion;
|
||||
|
||||
public void initializeView() {
|
||||
List<KeystoreFileImport> importers = Collections.emptyList();
|
||||
List<KeystoreFileImport> fileImporters = Collections.emptyList();
|
||||
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
|
||||
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
||||
fileImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
||||
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
|
||||
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
||||
fileImporters = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new Jade(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new GordianSeedTool(), new SpecterDIY());
|
||||
}
|
||||
|
||||
for(KeystoreFileImport importer : importers) {
|
||||
for(KeystoreFileImport importer : fileImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileKeystoreImportPane importPane = new FileKeystoreImportPane(getMasterController().getWallet(), importer, getMasterController().getRequiredDerivation());
|
||||
if(getMasterController().getRequiredModel() == null || getMasterController().getRequiredModel() == importer.getWalletModel()) {
|
||||
@@ -32,6 +40,19 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
}
|
||||
}
|
||||
|
||||
List<KeystoreCardImport> cardImporters = Collections.emptyList();
|
||||
if(isReaderAvailable()) {
|
||||
cardImporters = List.of(new Tapsigner());
|
||||
}
|
||||
for(KeystoreCardImport importer : cardImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
CardImportPane importPane = new CardImportPane(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()));
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user