mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68238e4e88 | ||
|
|
80fab6df99 | ||
|
|
6efe5e4ccc | ||
|
|
4c36d27d17 | ||
|
|
cc8dd59dbc | ||
|
|
7e91f57a42 | ||
|
|
f4c8bfa48c | ||
|
|
e0a14fdea6 | ||
|
|
b4af3586dc | ||
|
|
28722d385b | ||
|
|
dab6b9663a | ||
|
|
4e08334a3a | ||
|
|
dcb261a631 | ||
|
|
258fe34101 | ||
|
|
04917c45b6 | ||
|
|
08934d3c3c | ||
|
|
192657fa69 | ||
|
|
9c40c56c6c | ||
|
|
c4d0e4bac9 | ||
|
|
a62b14c8e4 | ||
|
|
3ec800e5e8 | ||
|
|
f30da06aaf | ||
|
|
03e9d23fa8 | ||
|
|
7dae9496ba | ||
|
|
b621cac18d | ||
|
|
13a576e871 | ||
|
|
19551671bd | ||
|
|
ecf9b78753 | ||
|
|
22303a2efc | ||
|
|
60aa20ac55 | ||
|
|
11cda40a40 | ||
|
|
ebbc4289e1 | ||
|
|
ac64811b35 | ||
|
|
7408744100 | ||
|
|
326fae88af | ||
|
|
0be73efdc1 | ||
|
|
486027f153 | ||
|
|
e42fc9a033 | ||
|
|
fc52670b2d | ||
|
|
4217de15a3 | ||
|
|
2d42ebff13 | ||
|
|
e629a51901 | ||
|
|
91273c2192 | ||
|
|
f8fce02a3d | ||
|
|
94c5920c27 | ||
|
|
c2eb505bd9 | ||
|
|
930e36fa2b | ||
|
|
9022438397 | ||
|
|
ba9aed5395 | ||
|
|
c780a8d944 | ||
|
|
3ed15ffefe | ||
|
|
ededb107a3 | ||
|
|
aebf4f9d28 | ||
|
|
336d0e551b | ||
|
|
5da9532614 | ||
|
|
956c2eaaaa | ||
|
|
62e7c34eb5 | ||
|
|
15da62777e | ||
|
|
3f2db7a199 | ||
|
|
adc9905038 | ||
|
|
fa82e1146b | ||
|
|
33a61e3414 | ||
|
|
70eda16515 | ||
|
|
e2eb7d3fa9 | ||
|
|
8aa0461d83 | ||
|
|
0f2cf9c5bb | ||
|
|
2c1f7e181c | ||
|
|
3555a0bd85 | ||
|
|
d7ce58d810 | ||
|
|
b0e1f6fe32 | ||
|
|
a324224e2a | ||
|
|
ddcb3e6f61 | ||
|
|
f4259642b8 | ||
|
|
db60afd13b | ||
|
|
f176a2a04f | ||
|
|
82be3a52dc | ||
|
|
4b2b8f653a | ||
|
|
555e5ecfb8 | ||
|
|
c0ca74ce6a | ||
|
|
674498052f | ||
|
|
1399b73dc2 | ||
|
|
c51f3d9e66 | ||
|
|
766a8c267f | ||
|
|
948d663fbf | ||
|
|
66be5c43a6 | ||
|
|
e0b00513b9 | ||
|
|
218761c594 | ||
|
|
5e4d6d5a78 | ||
|
|
b06df383dd | ||
|
|
361e92c600 | ||
|
|
69d0a2f96e | ||
|
|
bf078b2ea0 | ||
|
|
b5fa8f0ee0 | ||
|
|
7fb230e56b | ||
|
|
a53ecc4fdc | ||
|
|
3955eaaa3c | ||
|
|
d1d090a12b | ||
|
|
cd1509749a | ||
|
|
dd5278f442 | ||
|
|
631f5d48df | ||
|
|
c981cf32b9 | ||
|
|
984cabfc03 | ||
|
|
8d28f8f0a9 | ||
|
|
472fccc788 | ||
|
|
b1e715b272 | ||
|
|
6931cf7a45 | ||
|
|
1ccfc3c042 | ||
|
|
dd1976f173 | ||
|
|
eceaf40430 | ||
|
|
e565786bbc | ||
|
|
37a8a0a7f9 | ||
|
|
6aa3bb2ff3 | ||
|
|
516ee26ba0 | ||
|
|
1224abcd1d |
@@ -7,20 +7,20 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 16.0.1
|
||||
uses: actions/setup-java@v2
|
||||
- name: Set up JDK 18.0.1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '16.0.1'
|
||||
distribution: 'temurin'
|
||||
java-version: '18.0.1'
|
||||
- name: Show Build Versions
|
||||
run: ./gradlew -v
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew jpackage
|
||||
- name: Package zip distribution
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
if: ${{ runner.os == 'Windows' || runner.os == 'macOS' }}
|
||||
run: ./gradlew packageZipDistribution
|
||||
- name: Package tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
|
||||
@@ -16,8 +16,8 @@ or for those without SSH credentials:
|
||||
|
||||
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
|
||||
|
||||
In order to build, Sparrow requires Java 16 to be installed.
|
||||
The release binaries are built with [AdoptOpenJdk 16.0.1+9 Hotspot](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
|
||||
In order to build, Sparrow requires Java 17 to be installed.
|
||||
The release binaries are built with [Eclipse Temurin 17.0.2+8](https://github.com/adoptium/temurin17-binaries/releases/tag/jdk-17.0.2%2B8).
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
|
||||
@@ -28,6 +28,8 @@ The Sparrow binaries can be built from source using
|
||||
|
||||
`./gradlew jpackage`
|
||||
|
||||
Note that to build the Windows installer, you will need to install [WiX](https://github.com/wixtoolset/wix3/releases).
|
||||
|
||||
When updating to the latest HEAD
|
||||
|
||||
`git pull --recurse-submodules`
|
||||
@@ -42,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
|
||||
|
||||
`./sparrow`
|
||||
|
||||
Java 16 or higher must be installed.
|
||||
Java 17 or higher must be installed.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
+26
-20
@@ -1,16 +1,22 @@
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.10'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.13'
|
||||
id 'extra-java-module-info'
|
||||
id 'org.beryx.jlink' version '2.24.0'
|
||||
id 'org.beryx.jlink' version '2.25.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.6.3'
|
||||
def sparrowVersion = '1.6.6'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
osName = "osx"
|
||||
}
|
||||
def targetName = ""
|
||||
def osArch = "x64"
|
||||
if(System.getProperty("os.arch") == "aarch64") {
|
||||
osArch = "aarch64"
|
||||
targetName = "-" + osArch
|
||||
}
|
||||
|
||||
group "com.sparrowwallet"
|
||||
version "${sparrowVersion}"
|
||||
@@ -29,7 +35,7 @@ tasks.withType(AbstractArchiveTask) {
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = "17"
|
||||
version = "18"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
@@ -45,7 +51,7 @@ dependencies {
|
||||
}
|
||||
implementation('com.google.guava:guava:28.2-jre')
|
||||
implementation('com.google.code.gson:gson:2.8.6')
|
||||
implementation('com.h2database:h2:2.0.206')
|
||||
implementation('com.h2database:h2:2.1.214')
|
||||
implementation('com.zaxxer:HikariCP:4.0.3')
|
||||
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
@@ -67,13 +73,13 @@ dependencies {
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.4')
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
||||
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
}
|
||||
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
|
||||
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
|
||||
exclude group: 'com.nativelibs4java', module: 'bridj'
|
||||
}
|
||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8") {
|
||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}${targetName}:0.6.8") {
|
||||
exclude group: 'org.jetbrains.kotlin'
|
||||
}
|
||||
implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20')
|
||||
@@ -92,7 +98,7 @@ dependencies {
|
||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.32')
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.33')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
@@ -118,13 +124,13 @@ compileJava {
|
||||
processResources {
|
||||
doLast {
|
||||
delete fileTree("$buildDir/resources/main/native").matching {
|
||||
exclude "${osName}/**"
|
||||
exclude "${osName}/${osArch}/**"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
|
||||
jvmArgs '--add-opens=java.base/java.io=com.google.gson'
|
||||
}
|
||||
|
||||
run {
|
||||
@@ -212,7 +218,7 @@ jlink {
|
||||
appVersion = "${sparrowVersion}"
|
||||
skipInstaller = os.macOsX || properties.skipInstallers
|
||||
imageOptions = []
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--license-file', 'LICENSE']
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
|
||||
if(os.windows) {
|
||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
||||
@@ -239,6 +245,7 @@ task packageZipDistribution(type: Zip) {
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
from("$buildDir/jpackage/") {
|
||||
include "Sparrow/**"
|
||||
include "Sparrow.app/**"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,12 +336,12 @@ extraJavaModuleInfo {
|
||||
requires('org.slf4j')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
}
|
||||
module('bridj-0.7-20140918-3.jar', 'com.nativelibs4java.bridj', '0.7-20140918-3') {
|
||||
module("bridj${targetName}-0.7-20140918-3.jar", 'com.nativelibs4java.bridj', '0.7-20140918-3') {
|
||||
exports('org.bridj')
|
||||
exports('org.bridj.cpp')
|
||||
requires('java.logging')
|
||||
}
|
||||
module('webcam-capture-0.3.13-SNAPSHOT.jar', 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
|
||||
module("webcam-capture${targetName}-0.3.13-SNAPSHOT.jar", 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
|
||||
exports('com.github.sarxos.webcam')
|
||||
exports('com.github.sarxos.webcam.ds.buildin')
|
||||
exports('com.github.sarxos.webcam.ds.buildin.natives')
|
||||
@@ -453,6 +460,8 @@ extraJavaModuleInfo {
|
||||
module('kotlin-stdlib-jdk7-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk7', '1.5.20')
|
||||
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
|
||||
exports('kotlin')
|
||||
exports('kotlin.jvm')
|
||||
exports('kotlin.collections')
|
||||
}
|
||||
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
|
||||
exports('com.sparrowwallet.hummingbird')
|
||||
@@ -462,7 +471,7 @@ extraJavaModuleInfo {
|
||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||
exports('co.nstant.in.cbor')
|
||||
}
|
||||
module('nightjar-0.2.32.jar', 'com.sparrowwallet.nightjar', '0.2.32') {
|
||||
module('nightjar-0.2.33.jar', 'com.sparrowwallet.nightjar', '0.2.33') {
|
||||
requires('com.google.common')
|
||||
requires('net.sourceforge.streamsupport')
|
||||
requires('org.slf4j')
|
||||
@@ -550,7 +559,7 @@ extraJavaModuleInfo {
|
||||
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
|
||||
exports('net.jcip.annotations')
|
||||
}
|
||||
module("netlayer-jpms-${osName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
|
||||
module("netlayer-jpms-${osName}${targetName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
|
||||
exports('org.berndpruenster.netlayer.tor')
|
||||
requires('com.github.ravn.jsocks')
|
||||
requires('com.github.JesusMcCloud.jtorctl')
|
||||
@@ -578,15 +587,12 @@ extraJavaModuleInfo {
|
||||
module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
|
||||
requires('java.xml')
|
||||
}
|
||||
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
|
||||
exports('kotlin.jvm')
|
||||
exports('kotlin.collections')
|
||||
}
|
||||
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
|
||||
exports('com.beust.jcommander')
|
||||
}
|
||||
module('junit-4.12.jar', 'junit', '4.12') {
|
||||
exports('org.junit')
|
||||
requires('org.hamcrest.core')
|
||||
}
|
||||
module('hamcrest-core-1.3.jar', 'org.hamcrest.core', '1.3')
|
||||
}
|
||||
+2
@@ -67,6 +67,8 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
|
||||
outputs.file(originalJar);
|
||||
} else if (automaticModules.containsKey(originalJarName)) {
|
||||
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
|
||||
} else if(originalJarName.startsWith("kotlin-stdlib-common")) {
|
||||
//ignore
|
||||
} else {
|
||||
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
|
||||
}
|
||||
|
||||
+12
-14
@@ -12,32 +12,30 @@ Work on resolving both of these issues is ongoing.
|
||||
#### Install Java
|
||||
|
||||
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
|
||||
For v1.5.0 and later, this is AdoptOpenJdk jdk-16.0.1+9 Hotspot.
|
||||
It is available for all supported platforms from the [AdoptOpenJdk site](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
|
||||
For v1.6.4 and later, this is Eclipse Temurin 17.0.2+8.
|
||||
It is available for all supported platforms from [Eclipse Temurin 17.0.2+8](https://github.com/adoptium/temurin17-binaries/releases/tag/jdk-17.0.2%2B8).
|
||||
|
||||
For reference, the downloads are as follows:
|
||||
- [Linux x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz)
|
||||
- [MacOS x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_mac_hotspot_16.0.1_9.tar.gz)
|
||||
- [Windows x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_windows_hotspot_16.0.1_9.zip)
|
||||
- [Linux x64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.2_8.tar.gz)
|
||||
- [MacOS x64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_mac_hotspot_17.0.2_8.tar.gz)
|
||||
- [MacOS aarch64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_aarch64_mac_hotspot_17.0.2_8.tar.gz)
|
||||
- [Windows x64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_windows_hotspot_17.0.2_8.zip)
|
||||
|
||||
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
|
||||
```shell
|
||||
sudo apt-get install -y wget apt-transport-https gnupg
|
||||
wget https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public
|
||||
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --import public
|
||||
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --export --output adoptopenjdk-archive-keyring.gpg
|
||||
rm adoptopenjdk-keyring.gpg
|
||||
sudo mv adoptopenjdk-archive-keyring.gpg /usr/share/keyrings
|
||||
echo "deb [signed-by=/usr/share/keyrings/adoptopenjdk-archive-keyring.gpg] https://adoptopenjdk.jfrog.io/adoptopenjdk/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/adoptopenjdk.list
|
||||
wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add -
|
||||
echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
|
||||
sudo apt update -y
|
||||
sudo apt-get install -y adoptopenjdk-16-hotspot=16.0.1+9-3
|
||||
sudo apt-get install -y temurin-17-jdk=17.0.2+8
|
||||
sudo update-alternatives --config java
|
||||
```
|
||||
|
||||
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
|
||||
See the installation [instructions here](https://sdkman.io/install).
|
||||
Once installed, run
|
||||
```shell
|
||||
sdk install java 16.0.1.hs-adpt
|
||||
sdk install java 17.0.2-tem
|
||||
```
|
||||
|
||||
#### Other requirements
|
||||
@@ -51,7 +49,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||
|
||||
The project can cloned for a specific release tag as follows:
|
||||
```shell
|
||||
GIT_TAG="1.5.2"
|
||||
GIT_TAG="1.6.6"
|
||||
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
|
||||
```
|
||||
|
||||
|
||||
+1
-1
Submodule drongo updated: 20f4ac9657...8cdea77562
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -17,67 +17,101 @@
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# 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
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# 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"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
@@ -106,80 +140,101 @@ location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
Vendored
+8
-6
@@ -14,7 +14,7 @@
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +25,7 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
mime-type=x-scheme-handler/auth47
|
||||
description=Auth47 Authentication URI
|
||||
@@ -0,0 +1,2 @@
|
||||
mime-type=x-scheme-handler/lightning
|
||||
description=LNURL URI
|
||||
@@ -6,4 +6,4 @@ Icon=/opt/sparrow/lib/Sparrow.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Unknown
|
||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin
|
||||
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.6.3</string>
|
||||
<string>1.6.6</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
@@ -45,6 +45,22 @@
|
||||
<string>bitcoin</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.sparrowwallet.sparrow.auth47</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>auth47</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.sparrowwallet.sparrow.lightning</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>lightning</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
|
||||
@@ -77,6 +77,16 @@
|
||||
|
||||
<DirectoryRef Id="TARGETDIR">
|
||||
<Component Id="RegistryEntries" Guid="{206C911C-56EF-44B8-9257-5FD214427965}">
|
||||
<RegistryKey Root="HKCR" Key="auth47" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:Auth47 Authentication URI"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="HKCR" Key="bitcoin" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:Bitcoin Payment URL"/>
|
||||
@@ -87,6 +97,16 @@
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="HKCR" Key="lightning" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:LNURL URI"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.io.ByteSource;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.crypto.Key;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
@@ -90,6 +88,7 @@ public class AppController implements Initializable {
|
||||
public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
|
||||
public static final String CONNECTION_FAILED_PREFIX = "Connection failed: ";
|
||||
public static final String TRYING_ANOTHER_SERVER_MESSAGE = "trying another server...";
|
||||
public static final String JPACKAGE_APP_PATH = "jpackage.app-path";
|
||||
|
||||
@FXML
|
||||
private MenuItem saveTransaction;
|
||||
@@ -109,6 +108,12 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem exportWallet;
|
||||
|
||||
@FXML
|
||||
private MenuItem deleteWallet;
|
||||
|
||||
@FXML
|
||||
private MenuItem closeTab;
|
||||
|
||||
@FXML
|
||||
private Menu fileMenu;
|
||||
|
||||
@@ -156,6 +161,9 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem lockWallet;
|
||||
|
||||
@FXML
|
||||
private MenuItem lockAllWallets;
|
||||
|
||||
@FXML
|
||||
private MenuItem searchWallet;
|
||||
|
||||
@@ -178,6 +186,9 @@ public class AppController implements Initializable {
|
||||
private CheckMenuItem preventSleep;
|
||||
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private MenuItem restart;
|
||||
|
||||
@FXML
|
||||
private StackPane rootStack;
|
||||
|
||||
@@ -286,11 +297,13 @@ public class AppController implements Initializable {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -331,6 +344,8 @@ public class AppController implements Initializable {
|
||||
showLoadingLog.selectedProperty().bindBidirectional(showLoadingLogProperty);
|
||||
preventSleepProperty.set(Config.get().isPreventSleep());
|
||||
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
|
||||
restart.setText("Restart in " + (Network.get() == Network.MAINNET ? Network.TESTNET.toDisplayString() : Network.MAINNET.toDisplayString()));
|
||||
restart.setVisible(System.getProperty(JPACKAGE_APP_PATH) != null);
|
||||
|
||||
saveTransaction.setDisable(true);
|
||||
showTransaction.visibleProperty().bind(Bindings.and(saveTransaction.visibleProperty(), saveTransaction.disableProperty().not()));
|
||||
@@ -339,6 +354,8 @@ public class AppController implements Initializable {
|
||||
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
|
||||
showPSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
|
||||
exportWallet.setDisable(true);
|
||||
deleteWallet.disableProperty().bind(exportWallet.disableProperty());
|
||||
closeTab.setDisable(true);
|
||||
lockWallet.setDisable(true);
|
||||
searchWallet.disableProperty().bind(exportWallet.disableProperty());
|
||||
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
|
||||
@@ -409,6 +426,10 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void openSupport(ActionEvent event) {
|
||||
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/opensupport");
|
||||
}
|
||||
|
||||
public void submitBugReport(ActionEvent event) {
|
||||
AppServices.get().getApplication().getHostServices().showDocument("https://sparrowwallet.com/submitbugreport");
|
||||
}
|
||||
@@ -689,7 +710,7 @@ public class AppController implements Initializable {
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
if(!asText && !file.getName().toLowerCase().endsWith(".psbt")) {
|
||||
if(!asText && !file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt")) {
|
||||
file = new File(file.getAbsolutePath() + ".psbt");
|
||||
}
|
||||
|
||||
@@ -782,6 +803,10 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteWallet(ActionEvent event) {
|
||||
deleteWallet(getSelectedWalletForm());
|
||||
}
|
||||
|
||||
public void closeTab(ActionEvent event) {
|
||||
tabs.getTabs().remove(tabs.getSelectionModel().getSelectedItem());
|
||||
}
|
||||
@@ -836,6 +861,31 @@ public class AppController implements Initializable {
|
||||
AppServices.get().setPreventSleep(item.isSelected());
|
||||
}
|
||||
|
||||
public void restart(ActionEvent event) {
|
||||
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
|
||||
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
|
||||
}
|
||||
|
||||
Args args = new Args();
|
||||
ProcessHandle.current().info().arguments().ifPresent(argv -> {
|
||||
JCommander jCommander = JCommander.newBuilder().addObject(args).acceptUnknownOptions(true).build();
|
||||
jCommander.parse(argv);
|
||||
});
|
||||
|
||||
args.network = (Network.get() == Network.MAINNET ? Network.TESTNET : Network.MAINNET);
|
||||
|
||||
try {
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
||||
cmd.addAll(args.toParams());
|
||||
final ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
builder.start();
|
||||
quit(event);
|
||||
} catch(Exception e) {
|
||||
log.error("Error restarting application", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void openFile(File file) {
|
||||
if(isWalletFile(file)) {
|
||||
openWalletFile(file, true);
|
||||
@@ -965,7 +1015,9 @@ public class AppController implements Initializable {
|
||||
for(Map.Entry<WalletAndKey, Storage> entry : walletAndKey.getChildWallets().entrySet()) {
|
||||
openWallet(entry.getValue(), entry.getKey(), walletAppController, true);
|
||||
}
|
||||
Platform.runLater(() -> selectTab(walletAndKey.getWallet()));
|
||||
if(walletAndKey.getWallet().isMasterWallet()) {
|
||||
Platform.runLater(() -> selectTab(walletAndKey.getWallet()));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage());
|
||||
@@ -986,7 +1038,7 @@ public class AppController implements Initializable {
|
||||
Wallet copy = wallet.copy();
|
||||
for(int i = 0; i < copy.getKeystores().size(); i++) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
if(copyKeystore.hasSeed()) {
|
||||
if(copyKeystore.hasSeed() && copyKeystore.getSeed().getPassphrase() == null) {
|
||||
if(copyKeystore.getSeed().needsPassphrase()) {
|
||||
if(!wallet.isMasterWallet() && wallet.getMasterWallet().getKeystores().size() == copy.getKeystores().size() && wallet.getMasterWallet().getKeystores().get(i).hasSeed()) {
|
||||
copyKeystore.getSeed().setPassphrase(wallet.getMasterWallet().getKeystores().get(i).getSeed().getPassphrase());
|
||||
@@ -1051,8 +1103,13 @@ public class AppController implements Initializable {
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isBip47()) {
|
||||
try {
|
||||
Keystore masterKeystore = wallet.getKeystores().get(0);
|
||||
Keystore keystore = childWallet.getKeystores().get(0);
|
||||
keystore.setBip47ExtendedPrivateKey(wallet.getKeystores().get(0).getBip47ExtendedPrivateKey());
|
||||
keystore.setBip47ExtendedPrivateKey(masterKeystore.getBip47ExtendedPrivateKey());
|
||||
List<ChildNumber> derivation = keystore.getKeyDerivation().getDerivation();
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterKeystore.getKeyDerivation().getMasterFingerprint(), derivation));
|
||||
DeterministicKey pubKey = keystore.getBip47ExtendedPrivateKey().getKey().dropPrivateBytes().dropParent();
|
||||
keystore.setExtendedPublicKey(new ExtendedKey(pubKey, keystore.getBip47ExtendedPrivateKey().getParentFingerprint(), derivation.get(derivation.size() - 1)));
|
||||
} catch(Exception e) {
|
||||
log.error("Cannot prepare BIP47 keystore", e);
|
||||
}
|
||||
@@ -1382,6 +1439,17 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean allWalletsLocked(Wallet lockingWallet) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData instanceof WalletTabData walletTabData && walletTabData.getWallet() != lockingWallet && !walletTabData.getWalletForm().isLocked()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void searchWallet(ActionEvent event) {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
if(selectedTab != null) {
|
||||
@@ -1672,6 +1740,10 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
private void addTransactionTab(Transaction transaction, TransactionView initialView, Integer initialIndex) {
|
||||
addTransactionTab(null, null, transaction, null, null, initialView, initialIndex);
|
||||
}
|
||||
|
||||
private void addTransactionTab(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||
addTransactionTab(null, null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex);
|
||||
}
|
||||
@@ -1805,10 +1877,65 @@ public class AppController implements Initializable {
|
||||
tabs.getTabs().removeAll(tabs.getTabs());
|
||||
});
|
||||
|
||||
contextMenu.getItems().addAll(close, closeOthers, closeAll);
|
||||
MenuItem delete = new MenuItem("Delete...");
|
||||
delete.setOnAction(event -> {
|
||||
deleteWallet(getSelectedWalletForm());
|
||||
});
|
||||
|
||||
contextMenu.getItems().addAll(close, closeOthers, closeAll, delete);
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
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) {
|
||||
Storage storage = selectedWalletForm.getStorage();
|
||||
if(selectedWalletForm.getMasterWallet().isEncrypted()) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(selectedWalletForm.getWallet().getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
|
||||
try {
|
||||
tabs.getTabs().remove(tabs.getSelectionModel().getSelectedItem());
|
||||
deleteStorage(storage);
|
||||
} finally {
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(() -> deleteWallet(getSelectedWalletForm()));
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
tabs.getTabs().remove(tabs.getSelectionModel().getSelectedItem());
|
||||
deleteStorage(storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteStorage(Storage storage) {
|
||||
if(storage.isClosed()) {
|
||||
Platform.runLater(storage::delete);
|
||||
} else {
|
||||
Platform.runLater(() -> deleteStorage(storage));
|
||||
}
|
||||
}
|
||||
|
||||
private ContextMenu getSubTabContextMenu(Wallet wallet, TabPane subTabs, Tab subTab) {
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
MenuItem rename = new MenuItem("Rename Account");
|
||||
@@ -1833,7 +1960,7 @@ public class AppController implements Initializable {
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.OK) {
|
||||
subTabs.getTabs().remove(subTab);
|
||||
if(subTabs.getTabs().size() == 1) {
|
||||
setSubTabsVisible(subTabs, false);
|
||||
setSubTabsVisible(subTabs, areSubTabsVisible());
|
||||
}
|
||||
EventManager.get().post(new WalletDeletedEvent(wallet));
|
||||
}
|
||||
@@ -1872,7 +1999,7 @@ public class AppController implements Initializable {
|
||||
private void serverToggleStartAnimation() {
|
||||
Node thumbArea = serverToggle.lookup(".thumb-area");
|
||||
if(thumbArea != null) {
|
||||
Timeline timeline = AnimationUtil.getPulse(thumbArea, Duration.millis(600), 1.0, 0.4, 8);
|
||||
Timeline timeline = AnimationUtil.getPulse(thumbArea, Duration.millis(600), 1.0, 0.25, 8);
|
||||
timeline.play();
|
||||
serverToggle.setUserData(new AnimationUtil.AnimatedNode(thumbArea, timeline));
|
||||
}
|
||||
@@ -1908,6 +2035,7 @@ public class AppController implements Initializable {
|
||||
Label tabLabel = (Label)tab.getGraphic();
|
||||
if(!tabLabel.getStyleClass().contains("failure")) {
|
||||
tabLabel.getGraphic().getStyleClass().add("failure");
|
||||
tabLabel.setTooltip(new Tooltip("Error loading transaction history from server"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1940,6 +2068,7 @@ public class AppController implements Initializable {
|
||||
private void tabLabelRemoveFailure(Tab tab) {
|
||||
Label tabLabel = (Label)tab.getGraphic();
|
||||
tabLabel.getGraphic().getStyleClass().remove("failure");
|
||||
tabLabel.setTooltip(null);
|
||||
}
|
||||
|
||||
private void setTorIcon() {
|
||||
@@ -2013,6 +2142,7 @@ public class AppController implements Initializable {
|
||||
saveTransaction.setDisable(true);
|
||||
lockWallet.setDisable(walletTabData.getWalletForm().lockedProperty().get());
|
||||
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid() || walletTabData.getWalletForm().isLocked());
|
||||
refreshWallet.setText(walletTabData.getWallet() == null || walletTabData.getWalletForm().getMasterWallet().getChildWallets().stream().allMatch(Wallet::isNested) ? "Refresh Wallet" : "Refresh Wallet Account");
|
||||
showLoadingLog.setDisable(false);
|
||||
showTxHex.setDisable(true);
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode());
|
||||
@@ -2083,7 +2213,7 @@ public class AppController implements Initializable {
|
||||
if(!whirlpoolTransactions.isEmpty()) {
|
||||
BlockTransaction blockTransaction = whirlpoolTransactions.get(0);
|
||||
String status;
|
||||
String walletName = event.getWallet().getMasterName() + " " + event.getWallet().getName().toLowerCase();
|
||||
String walletName = event.getWallet().getMasterName() + " " + event.getWallet().getName().toLowerCase(Locale.ROOT);
|
||||
long value = blockTransaction.getTransaction().getOutputs().iterator().next().getValue();
|
||||
long mempoolValue = whirlpoolTransactions.stream().filter(tx -> tx.getHeight() <= 0).mapToLong(tx -> value).sum();
|
||||
long blockchainValue = whirlpoolTransactions.stream().filter(tx -> tx.getHeight() > 0).mapToLong(tx -> value).sum();
|
||||
@@ -2472,7 +2602,11 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void viewTransaction(ViewTransactionEvent event) {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex());
|
||||
if(event.getBlockTransaction() != null) {
|
||||
addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex());
|
||||
} else {
|
||||
addTransactionTab(event.getTransaction(), event.getInitialView(), event.getInitialIndex());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2557,6 +2691,8 @@ public class AppController implements Initializable {
|
||||
lockWallet.setDisable(true);
|
||||
exportWallet.setDisable(true);
|
||||
}
|
||||
|
||||
lockAllWallets.setDisable(allWalletsLocked(event.getWallet()));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -2565,6 +2701,7 @@ public class AppController implements Initializable {
|
||||
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
|
||||
lockWallet.setDisable(false);
|
||||
exportWallet.setDisable(!event.getWallet().isValid());
|
||||
lockAllWallets.setDisable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,23 @@ package com.sparrowwallet.sparrow;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.crypto.Key;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.Auth47;
|
||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
@@ -46,6 +53,7 @@ import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
import org.berndpruenster.netlayer.tor.Tor;
|
||||
import org.controlsfx.control.HyperlinkLabel;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -70,9 +78,11 @@ public class AppServices {
|
||||
|
||||
private static final int SERVER_PING_PERIOD_SECS = 60;
|
||||
private static final int PUBLIC_SERVER_RETRY_PERIOD_SECS = 3;
|
||||
private static final int ENUMERATE_HW_PERIOD_SECS = 30;
|
||||
private static final int PRIVATE_SERVER_RETRY_PERIOD_SECS = 15;
|
||||
public static final int ENUMERATE_HW_PERIOD_SECS = 30;
|
||||
private static final int RATES_PERIOD_SECS = 5 * 60;
|
||||
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
|
||||
private static final int CONNECTION_DELAY_SECS = 2;
|
||||
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
|
||||
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
|
||||
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
||||
@@ -141,7 +151,9 @@ public class AppServices {
|
||||
torService.reset();
|
||||
}
|
||||
|
||||
torService.start();
|
||||
if(torService.getState() != Worker.State.RUNNING) {
|
||||
torService.start();
|
||||
}
|
||||
} else {
|
||||
restartServices();
|
||||
}
|
||||
@@ -247,11 +259,14 @@ public class AppServices {
|
||||
|
||||
private ElectrumServer.ConnectionService createConnectionService() {
|
||||
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService();
|
||||
//Delay startup on first connection to Bitcoin Core to allow any unencrypted wallets to open first
|
||||
connectionService.setDelay(Config.get().getServerType() == ServerType.BITCOIN_CORE ? Duration.seconds(CONNECTION_DELAY_SECS) : Duration.ZERO);
|
||||
connectionService.setPeriod(Duration.seconds(SERVER_PING_PERIOD_SECS));
|
||||
connectionService.setRestartOnFailure(true);
|
||||
EventManager.get().register(connectionService);
|
||||
|
||||
connectionService.setOnRunning(workerStateEvent -> {
|
||||
connectionService.setDelay(Duration.ZERO);
|
||||
if(!ElectrumServer.isConnected()) {
|
||||
EventManager.get().post(new ConnectionStartEvent(Config.get().getServerAddress()));
|
||||
}
|
||||
@@ -317,6 +332,8 @@ public class AppServices {
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
|
||||
Config.get().changePublicServer();
|
||||
connectionService.setPeriod(Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS));
|
||||
} else {
|
||||
connectionService.setPeriod(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
|
||||
}
|
||||
|
||||
log.debug("Connection failed", failEvent.getSource().getException());
|
||||
@@ -358,7 +375,7 @@ public class AppServices {
|
||||
|
||||
private Hwi.ScheduledEnumerateService createDeviceEnumerateService() {
|
||||
Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null);
|
||||
enumerateService.setPeriod(Duration.seconds(ENUMERATE_HW_PERIOD_SECS));
|
||||
enumerateService.setPeriod(Duration.seconds(Config.get().getEnumerateHwPeriod()));
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
List<Device> devices = enumerateService.getValue();
|
||||
|
||||
@@ -690,6 +707,10 @@ public class AppServices {
|
||||
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
||||
}
|
||||
|
||||
public static void clearPayjoinURI(Address address) {
|
||||
payjoinURIs.remove(address);
|
||||
}
|
||||
|
||||
public static void clearTransactionHistoryCache(Wallet wallet) {
|
||||
ElectrumServer.clearRetrievedScriptHashes(wallet);
|
||||
|
||||
@@ -712,6 +733,14 @@ public class AppServices {
|
||||
return showAlertDialog(title, content == null ? "See log file (Help menu)" : content, Alert.AlertType.ERROR, buttons);
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showSuccessDialog(String title, String content, ButtonType... buttons) {
|
||||
Glyph successGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||
successGlyph.getStyleClass().add("success");
|
||||
successGlyph.setFontSize(50);
|
||||
|
||||
return showAlertDialog(title, content, Alert.AlertType.INFORMATION, successGlyph, buttons);
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {
|
||||
return showAlertDialog(title, content, alertType, null, buttons);
|
||||
}
|
||||
@@ -855,6 +884,10 @@ public class AppServices {
|
||||
Platform.runLater(() -> {
|
||||
if("bitcoin".equals(uri.getScheme())) {
|
||||
openBitcoinUri(uri);
|
||||
} else if(("auth47").equals(uri.getScheme())) {
|
||||
openAuth47Uri(uri);
|
||||
} else if(("lightning").equals(uri.getScheme())) {
|
||||
openLnurlAuthUri(uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -877,11 +910,13 @@ public class AppServices {
|
||||
private static void openBitcoinUri(URI uri) {
|
||||
try {
|
||||
BitcoinURI bitcoinURI = new BitcoinURI(uri.toString());
|
||||
Wallet wallet = selectWallet(null, "pay from");
|
||||
List<PolicyType> policyTypes = Arrays.asList(PolicyType.values());
|
||||
List<ScriptType> scriptTypes = Arrays.asList(ScriptType.ADDRESSABLE_TYPES);
|
||||
Wallet wallet = selectWallet(policyTypes, scriptTypes, true, false, "pay from", false);
|
||||
|
||||
if(wallet != null) {
|
||||
final Wallet sendingWallet = wallet;
|
||||
EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getWalletUtxos().keySet())));
|
||||
EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getWalletUtxos().keySet()), true));
|
||||
Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment()))));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
@@ -889,12 +924,97 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
private static Wallet selectWallet(ScriptType scriptType, String actionDescription) {
|
||||
private static void openAuth47Uri(URI uri) {
|
||||
try {
|
||||
Auth47 auth47 = new Auth47(uri);
|
||||
List<ScriptType> scriptTypes = PaymentCode.SEGWIT_SCRIPT_TYPES;
|
||||
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
|
||||
|
||||
if(wallet != null) {
|
||||
try {
|
||||
auth47.sendResponse(wallet);
|
||||
EventManager.get().post(new StatusEvent("Successfully authenticated to " + auth47.getCallback().getHost()));
|
||||
} catch(Exception e) {
|
||||
log.error("Error authenticating auth47 URI", e);
|
||||
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Not a valid auth47 URI", e);
|
||||
showErrorDialog("Not a valid auth47 URI", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static void openLnurlAuthUri(URI uri) {
|
||||
try {
|
||||
LnurlAuth lnurlAuth = new LnurlAuth(uri);
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
|
||||
|
||||
if(wallet != null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
copy.decrypt(key);
|
||||
try {
|
||||
lnurlAuth.sendResponse(copy);
|
||||
EventManager.get().post(new StatusEvent("Successfully authenticated to " + lnurlAuth.getDomain()));
|
||||
} catch(Exception e) {
|
||||
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||
} finally {
|
||||
key.clear();
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(() -> openLnurlAuthUri(uri));
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
lnurlAuth.sendResponse(wallet);
|
||||
EventManager.get().post(new StatusEvent("Successfully authenticated to " + lnurlAuth.getDomain()));
|
||||
} catch(LnurlAuth.LnurlAuthException e) {
|
||||
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||
} catch(Exception e) {
|
||||
log.error("Failed to authenticate using LNURL-auth", e);
|
||||
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Not a valid LNURL-auth URI", e);
|
||||
showErrorDialog("Not a valid LNURL-auth URI", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Wallet selectWallet(List<PolicyType> policyTypes, List<ScriptType> scriptTypes, boolean taprootAllowed, boolean privateKeysRequired, String actionDescription, boolean alwaysAsk) {
|
||||
Wallet wallet = null;
|
||||
List<Wallet> wallets = get().getOpenWallets().keySet().stream().filter(w -> scriptType == null || w.getScriptType() == scriptType).collect(Collectors.toList());
|
||||
List<Wallet> wallets = get().getOpenWallets().keySet().stream().filter(w -> w.isValid() && policyTypes.contains(w.getPolicyType()) && scriptTypes.contains(w.getScriptType())
|
||||
&& (!privateKeysRequired || w.getKeystores().stream().allMatch(Keystore::hasPrivateKey))).collect(Collectors.toList());
|
||||
if(wallets.isEmpty()) {
|
||||
showErrorDialog("No wallet available", "Open a" + (scriptType == null ? "" : " " + scriptType.getDescription()) + " wallet to " + actionDescription + ".");
|
||||
} else if(wallets.size() == 1) {
|
||||
boolean taprootOpen = get().getOpenWallets().keySet().stream().anyMatch(w -> w.getScriptType() == ScriptType.P2TR);
|
||||
showErrorDialog("No wallet available", "Open a" + (taprootOpen && !taprootAllowed ? " non-Taproot" : "") + (privateKeysRequired ? " software" : "") + " wallet to " + actionDescription + ".");
|
||||
} else if(wallets.size() == 1 && !alwaysAsk) {
|
||||
wallet = wallets.iterator().next();
|
||||
} else {
|
||||
ChoiceDialog<Wallet> walletChoiceDialog = new ChoiceDialog<>(wallets.iterator().next(), wallets);
|
||||
@@ -923,7 +1043,7 @@ public class AppServices {
|
||||
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
Config.get().addRecentServer();
|
||||
}
|
||||
@@ -1052,11 +1172,27 @@ public class AppServices {
|
||||
|
||||
@Subscribe
|
||||
public void walletOpening(WalletOpeningEvent event) {
|
||||
restartBwt(event.getWallet());
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
Platform.runLater(() -> restartBwt(event.getWallet()));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void childWalletsAdded(ChildWalletsAddedEvent event) {
|
||||
if(event.getChildWallets().stream().anyMatch(Wallet::isNested)) {
|
||||
restartBwt(event.getWallet());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && event.getNestedHistoryChangedNodes().stream().anyMatch(node -> node.getTransactionOutputs().isEmpty())) {
|
||||
Platform.runLater(() -> restartBwt(event.getWallet()));
|
||||
}
|
||||
}
|
||||
|
||||
private void restartBwt(Wallet wallet) {
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && isConnected() && wallet.isValid()) {
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && connectionService != null && connectionService.isConnectionRunning() && wallet.isValid()) {
|
||||
connectionService.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import com.beust.jcommander.Parameter;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Args {
|
||||
@Parameter(names = { "--dir", "-d" }, description = "Path to Sparrow home folder")
|
||||
public String dir;
|
||||
@@ -16,4 +19,23 @@ public class Args {
|
||||
|
||||
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
|
||||
public boolean help;
|
||||
|
||||
public List<String> toParams() {
|
||||
List<String> params = new ArrayList<>();
|
||||
|
||||
if(dir != null) {
|
||||
params.add("-d");
|
||||
params.add(dir);
|
||||
}
|
||||
if(network != null) {
|
||||
params.add("-n");
|
||||
params.add(network.toString());
|
||||
}
|
||||
if(level != null) {
|
||||
params.add("-l");
|
||||
params.add(level.toString());
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.stream.Collectors;
|
||||
public class MainApp extends Application {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.6.3";
|
||||
public static final String APP_VERSION = "1.6.6";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
@@ -136,7 +136,7 @@ public class MainApp extends Application {
|
||||
|
||||
public static void main(String[] argv) {
|
||||
Args args = new Args();
|
||||
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase()).acceptUnknownOptions(true).build();
|
||||
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
|
||||
jCommander.parse(argv);
|
||||
if(args.help) {
|
||||
jCommander.usage();
|
||||
@@ -158,7 +158,7 @@ public class MainApp extends Application {
|
||||
String envNetwork = System.getenv(NETWORK_ENV_PROPERTY);
|
||||
if(envNetwork != null) {
|
||||
try {
|
||||
Network.set(Network.valueOf(envNetwork.toUpperCase()));
|
||||
Network.set(Network.valueOf(envNetwork.toUpperCase(Locale.ROOT)));
|
||||
} catch(Exception e) {
|
||||
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.wallet.Status;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletUtxoStatusChangedEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
public AddressCell() {
|
||||
super();
|
||||
@@ -37,10 +43,12 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
|
||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
|
||||
setTooltip(tooltip);
|
||||
|
||||
if(addressStatus.isDuplicate()) {
|
||||
if(addressStatus.isDustAttack()) {
|
||||
setGraphic(getDustAttackHyperlink(utxoEntry));
|
||||
} else if(addressStatus.isDuplicate()) {
|
||||
setGraphic(getDuplicateGlyph());
|
||||
} else {
|
||||
setGraphic(null);
|
||||
@@ -49,9 +57,9 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
}
|
||||
}
|
||||
|
||||
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
|
||||
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate, boolean dustAttack) {
|
||||
return (utxoEntry.getNode().getWallet().isNested() ? utxoEntry.getNode().getWallet().getDisplayName() + " " : "" ) +
|
||||
utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : "");
|
||||
utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : (dustAttack ? " (Possible dust attack)" : ""));
|
||||
}
|
||||
|
||||
public static Glyph getDuplicateGlyph() {
|
||||
@@ -60,4 +68,22 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
duplicateGlyph.setFontSize(12);
|
||||
return duplicateGlyph;
|
||||
}
|
||||
|
||||
public static Hyperlink getDustAttackHyperlink(UtxoEntry utxoEntry) {
|
||||
Glyph dustAttackGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
dustAttackGlyph.getStyleClass().add("dust-attack-warning");
|
||||
dustAttackGlyph.setFontSize(12);
|
||||
|
||||
Hyperlink hyperlink = new Hyperlink(utxoEntry.getHashIndex().getStatus() == Status.FROZEN ? "" : "Freeze?", dustAttackGlyph);
|
||||
hyperlink.getStyleClass().add("freeze-dust-utxo");
|
||||
hyperlink.setOnAction(event -> {
|
||||
if(utxoEntry.getHashIndex().getStatus() != Status.FROZEN) {
|
||||
hyperlink.setText("");
|
||||
utxoEntry.getHashIndex().setStatus(Status.FROZEN);
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(utxoEntry.getWallet(), Collections.singletonList(utxoEntry.getHashIndex())));
|
||||
}
|
||||
});
|
||||
|
||||
return hyperlink;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReceiveToEvent;
|
||||
import com.sparrowwallet.sparrow.event.ShowTransactionsCountEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||
import javafx.application.Platform;
|
||||
@@ -13,9 +15,7 @@ import javafx.collections.ListChangeListener;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseButton;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.*;
|
||||
|
||||
public class AddressTreeTable extends CoinTreeTable {
|
||||
public void initialize(NodeEntry rootEntry) {
|
||||
@@ -46,6 +46,15 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
labelCol.setSortable(false);
|
||||
getColumns().add(labelCol);
|
||||
|
||||
TreeTableColumn<Entry, Number> countCol = new TreeTableColumn<>("Transactions");
|
||||
countCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getChildren().size());
|
||||
});
|
||||
countCol.setCellFactory(p -> new NumberCell());
|
||||
countCol.setSortable(false);
|
||||
countCol.setVisible(Config.get().isShowAddressTransactionCount());
|
||||
getColumns().add(countCol);
|
||||
|
||||
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
|
||||
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
|
||||
@@ -54,6 +63,19 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
amountCol.setSortable(false);
|
||||
getColumns().add(amountCol);
|
||||
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
CheckMenuItem showCountItem = new CheckMenuItem("Show Transaction Count");
|
||||
contextMenu.setOnShowing(event -> {
|
||||
showCountItem.setSelected(Config.get().isShowAddressTransactionCount());
|
||||
});
|
||||
showCountItem.setOnAction(event -> {
|
||||
boolean show = !Config.get().isShowAddressTransactionCount();
|
||||
Config.get().setShowAddressTransactionCount(show);
|
||||
EventManager.get().post(new ShowTransactionsCountEvent(show));
|
||||
});
|
||||
contextMenu.getItems().add(showCountItem);
|
||||
getColumns().forEach(col -> col.setContextMenu(contextMenu));
|
||||
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
|
||||
@@ -106,17 +128,38 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
}
|
||||
|
||||
public void updateHistory(List<WalletNode> updatedNodes) {
|
||||
//We only ever add child nodes - never remove in order to keep a full sequence
|
||||
//We only ever add child nodes - never remove in order to keep a full sequence (unless hide empty used addresses is set)
|
||||
NodeEntry rootEntry = (NodeEntry)getRoot().getValue();
|
||||
|
||||
Map<WalletNode, NodeEntry> childNodes = new HashMap<>();
|
||||
for(Entry childEntry : rootEntry.getChildren()) {
|
||||
NodeEntry nodeEntry = (NodeEntry)childEntry;
|
||||
childNodes.put(nodeEntry.getNode(), nodeEntry);
|
||||
}
|
||||
|
||||
for(WalletNode updatedNode : updatedNodes) {
|
||||
Optional<Entry> optEntry = rootEntry.getChildren().stream().filter(childEntry -> ((NodeEntry)childEntry).getNode().equals(updatedNode)).findFirst();
|
||||
if(optEntry.isPresent()) {
|
||||
NodeEntry existingEntry = (NodeEntry)optEntry.get();
|
||||
NodeEntry existingEntry = childNodes.get(updatedNode);
|
||||
if(existingEntry != null) {
|
||||
existingEntry.refreshChildren();
|
||||
|
||||
if(Config.get().isHideEmptyUsedAddresses() && existingEntry.getValue() == 0L) {
|
||||
rootEntry.getChildren().remove(existingEntry);
|
||||
}
|
||||
} else {
|
||||
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
|
||||
rootEntry.getChildren().add(nodeEntry);
|
||||
|
||||
if(Config.get().isHideEmptyUsedAddresses()) {
|
||||
int index = 0;
|
||||
for( ; index < rootEntry.getChildren().size(); index++) {
|
||||
existingEntry = (NodeEntry)rootEntry.getChildren().get(index);
|
||||
if(nodeEntry.compareTo(existingEntry) < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
rootEntry.getChildren().add(index, nodeEntry);
|
||||
} else {
|
||||
rootEntry.getChildren().add(nodeEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,4 +170,8 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
Entry rootEntry = getRoot().getValue();
|
||||
rootEntry.updateLabel(entry);
|
||||
}
|
||||
|
||||
public void showTransactionsCount(boolean show) {
|
||||
getColumns().stream().filter(col -> col.getText().equals("Transactions")).forEach(col -> col.setVisible(show));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
private static final Pattern REPLACED_BY_FEE_SUFFIX = Pattern.compile("(.*)\\(Replaced By Fee( #)?(\\d+)?\\).*");
|
||||
|
||||
private static EntryCell lastCell;
|
||||
|
||||
public EntryCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
@@ -47,6 +49,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
protected void updateItem(Entry entry, boolean empty) {
|
||||
super.updateItem(entry, empty);
|
||||
|
||||
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
|
||||
if(this == lastCell && !getTableRow().isVisible()) {
|
||||
return;
|
||||
}
|
||||
lastCell = this;
|
||||
|
||||
applyRowStyles(this, entry);
|
||||
|
||||
if(empty) {
|
||||
@@ -206,14 +214,16 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||
Transaction tx = blockTransaction.getTransaction();
|
||||
double vSize = tx.getVirtualSize();
|
||||
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
|
||||
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
|
||||
//Remove any UTXOs created by the transaction that is to be replaced
|
||||
walletUtxos.removeIf(utxo -> ourOutputs.stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex()));
|
||||
Collections.shuffle(walletUtxos);
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty()) {
|
||||
//If there is insufficent change output, include another random UTXO so the fee can be increased
|
||||
//If there is insufficient change output, include another random UTXO so the fee can be increased
|
||||
BlockTransactionHashIndex utxo = walletUtxos.remove(0);
|
||||
utxos.add(utxo);
|
||||
changeTotal += utxo.getValue();
|
||||
@@ -223,6 +233,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
List<TransactionOutput> externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs());
|
||||
externalOutputs.removeAll(ourOutputs);
|
||||
externalOutputs.addAll(consolidationOutputs);
|
||||
final long rbfChange = changeTotal;
|
||||
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
|
||||
try {
|
||||
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
||||
@@ -240,7 +251,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
|
||||
if(txOutput.getScript().getToAddress() != null) {
|
||||
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), false);
|
||||
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
|
||||
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -361,11 +373,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
private String getTooltip(TransactionEntry transactionEntry) {
|
||||
String tooltip = transactionEntry.getBlockTransaction().getHash().toString();
|
||||
if(transactionEntry.getBlockTransaction().getHeight() <= 0) {
|
||||
if(!AppServices.getMempoolHistogram().isEmpty()) {
|
||||
Set<MempoolRateSize> rateSizes = AppServices.getMempoolHistogram().get(AppServices.getMempoolHistogram().lastKey());
|
||||
double vSize = transactionEntry.getBlockTransaction().getTransaction().getVirtualSize();
|
||||
double feeRate = transactionEntry.getBlockTransaction().getFee() / vSize;
|
||||
long vSizefromTip = rateSizes.stream().filter(rateSize -> rateSize.getFee() > feeRate).mapToLong(MempoolRateSize::getVSize).sum();
|
||||
Double feeRate = transactionEntry.getBlockTransaction().getFeeRate();
|
||||
Long vSizefromTip = transactionEntry.getVSizeFromTip();
|
||||
if(feeRate != null && vSizefromTip != null) {
|
||||
long blocksFromTip = (long)Math.ceil((double)vSizefromTip / Transaction.MAX_BLOCK_SIZE);
|
||||
|
||||
String amount = vSizefromTip + " vB";
|
||||
if(vSizefromTip > 1000 * 1000) {
|
||||
amount = String.format("%.2f", (double)vSizefromTip / (1000 * 1000)) + " MvB";
|
||||
@@ -373,7 +385,11 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
amount = String.format("%.2f", (double)vSizefromTip / 1000) + " kvB";
|
||||
}
|
||||
|
||||
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB (" + amount + " from tip)";
|
||||
tooltip += "\nConfirms in: " + (blocksFromTip > 1 ? blocksFromTip + "+ blocks" : "1 block") + " (" + amount + " from tip)";
|
||||
}
|
||||
|
||||
if(feeRate != null) {
|
||||
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
||||
}
|
||||
|
||||
tooltip += "\nRBF: " + (transactionEntry.getBlockTransaction().getTransaction().isReplaceByFee() ? "Enabled" : "Disabled");
|
||||
|
||||
@@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
private final KeyDerivation requiredDerivation;
|
||||
|
||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
@@ -34,4 +34,17 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
@@ -80,7 +81,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
|
||||
String extension = exporter.getExportFileExtension(wallet);
|
||||
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "");
|
||||
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase(Locale.ROOT).replace(" ", "");
|
||||
if(exporter instanceof Sparrow) {
|
||||
fileName = wallet.getMasterName();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.control.SpinnerValueFactory;
|
||||
import javafx.util.converter.IntegerStringConverter;
|
||||
|
||||
public class IntegerSpinner extends Spinner<Integer> {
|
||||
public IntegerSpinner() {
|
||||
super();
|
||||
setupEditor();
|
||||
}
|
||||
|
||||
public IntegerSpinner(@NamedArg("min") int min,
|
||||
@NamedArg("max") int max,
|
||||
@NamedArg("initialValue") int initialValue) {
|
||||
super(min, max, initialValue);
|
||||
setupEditor();
|
||||
}
|
||||
|
||||
public IntegerSpinner(@NamedArg("min") int min,
|
||||
@NamedArg("max") int max,
|
||||
@NamedArg("initialValue") int initialValue,
|
||||
@NamedArg("amountToStepBy") int amountToStepBy) {
|
||||
super(min, max, initialValue, amountToStepBy);
|
||||
setupEditor();
|
||||
}
|
||||
|
||||
private void setupEditor() {
|
||||
getEditor().focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null && !newValue) {
|
||||
commitValue();
|
||||
}
|
||||
});
|
||||
getEditor().textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(!newValue.matches("\\d*")) {
|
||||
getEditor().setText(newValue.replaceAll("[^\\d]", ""));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static class ValueFactory extends SpinnerValueFactory.IntegerSpinnerValueFactory {
|
||||
public ValueFactory(@NamedArg("min") int min,
|
||||
@NamedArg("max") int max) {
|
||||
super(min, max);
|
||||
setupConverter(min);
|
||||
}
|
||||
|
||||
public ValueFactory(@NamedArg("min") int min,
|
||||
@NamedArg("max") int max,
|
||||
@NamedArg("initialValue") int initialValue) {
|
||||
super(min, max, initialValue);
|
||||
setupConverter(initialValue);
|
||||
}
|
||||
|
||||
public ValueFactory(@NamedArg("min") int min,
|
||||
@NamedArg("max") int max,
|
||||
@NamedArg("initialValue") int initialValue,
|
||||
@NamedArg("amountToStepBy") int amountToStepBy) {
|
||||
super(min, max, initialValue, amountToStepBy);
|
||||
setupConverter(initialValue);
|
||||
}
|
||||
|
||||
private void setupConverter(Integer defaultValue) {
|
||||
setConverter(new IntegerStringConverter() {
|
||||
@Override
|
||||
public Integer fromString(String value) {
|
||||
if(value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
value = value.trim();
|
||||
|
||||
if(value.length() < 1) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return Integer.valueOf(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import tornadofx.control.Form;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
@@ -127,6 +128,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
address = new TextField();
|
||||
address.getStyleClass().add("id");
|
||||
address.setEditable(walletNode == null);
|
||||
address.setTooltip(new Tooltip("Only Legacy (P2PKH), Nested Segwit (P2SH-P2WPKH) and Native Segwit (P2WPKH) singlesig addresses can sign"));
|
||||
addressField.getInputs().add(address);
|
||||
|
||||
if(walletNode != null) {
|
||||
@@ -184,7 +186,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
if(buttons.length > 0) {
|
||||
dialogPane.getButtonTypes().addAll(buttons);
|
||||
|
||||
ButtonType customSignButtonType = Arrays.stream(buttons).filter(buttonType -> buttonType.getText().toLowerCase().contains("sign")).findFirst().orElse(null);
|
||||
ButtonType customSignButtonType = Arrays.stream(buttons).filter(buttonType -> buttonType.getText().toLowerCase(Locale.ROOT).contains("sign")).findFirst().orElse(null);
|
||||
if(customSignButtonType != null) {
|
||||
Button customSignButton = (Button)dialogPane.lookupButton(customSignButtonType);
|
||||
customSignButton.setDefaultButton(true);
|
||||
@@ -293,7 +295,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private boolean isValidAddress() {
|
||||
try {
|
||||
Address address = getAddress();
|
||||
return address.getScriptType() != ScriptType.P2TR;
|
||||
return address.getScriptType() != ScriptType.P2TR && address.getScriptType().isAllowed(PolicyType.SINGLE);
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -370,15 +372,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
if(verified) {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||
AppServices.setStageIcon(alert.getDialogPane().getScene().getWindow());
|
||||
alert.setTitle("Verification Succeeded");
|
||||
alert.setHeaderText("Verification Succeeded");
|
||||
alert.setContentText("The signature verified against the message.");
|
||||
AppServices.moveToActiveWindowScreen(alert);
|
||||
alert.showAndWait();
|
||||
AppServices.showSuccessDialog("Verification Succeeded", "The signature verified against the message.");
|
||||
} else {
|
||||
AppServices.showErrorDialog("Verification failed", "The provided signature did not match the message for this address.");
|
||||
AppServices.showErrorDialog("Verification Failed", "The provided signature did not match the message for this address.");
|
||||
}
|
||||
} catch(IllegalArgumentException e) {
|
||||
AppServices.showErrorDialog("Could not verify message", e.getMessage());
|
||||
|
||||
@@ -17,6 +17,8 @@ import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
|
||||
|
||||
@@ -114,7 +116,7 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1);
|
||||
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase(Locale.ROOT) + mixProgress.getMixStep().getMessage().substring(1);
|
||||
tt.setText(status);
|
||||
setTooltip(tt);
|
||||
|
||||
|
||||
@@ -17,25 +17,13 @@ import java.util.List;
|
||||
|
||||
public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
public MnemonicKeystoreDisplayPane(Keystore keystore) {
|
||||
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
showHideLink.setVisible(false);
|
||||
buttonBox.getChildren().clear();
|
||||
|
||||
showWordList(keystore.getSeed());
|
||||
}
|
||||
|
||||
private void showWordList(DeterministicSeed seed) {
|
||||
List<String> words = seed.getMnemonicCode();
|
||||
setContent(getMnemonicWordsEntry(words.size()));
|
||||
setExpanded(true);
|
||||
|
||||
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
|
||||
wordEntry.getEditor().setText(words.get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected Node getMnemonicWordsEntry(int numWords) {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
@@ -26,13 +28,16 @@ import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
|
||||
public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(MnemonicKeystorePane.class);
|
||||
|
||||
protected SplitMenuButton enterMnemonicButton;
|
||||
protected TilePane wordsPane;
|
||||
protected Label validLabel;
|
||||
@@ -73,9 +78,44 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
});
|
||||
enterMnemonicButton.getItems().add(item);
|
||||
}
|
||||
enterMnemonicButton.getItems().add(new SeparatorMenuItem());
|
||||
MenuItem scanItem = new MenuItem("Scan QR...");
|
||||
scanItem.setOnAction(event -> {
|
||||
scanQR();
|
||||
});
|
||||
enterMnemonicButton.getItems().add(scanItem);
|
||||
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
||||
}
|
||||
|
||||
protected void scanQR() {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||
if(optionalResult.isPresent()) {
|
||||
QRScanDialog.Result result = optionalResult.get();
|
||||
if(result.seed != null) {
|
||||
showWordList(result.seed);
|
||||
Platform.runLater(() -> validLabel.requestFocus());
|
||||
} else if(result.exception != null) {
|
||||
log.error("Error scanning QR", result.exception);
|
||||
showErrorDialog("Error scanning QR", result.exception.getMessage());
|
||||
} else {
|
||||
AppServices.showErrorDialog("Invalid QR Code", "Cannot parse QR code into a seed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void showWordList(DeterministicSeed seed) {
|
||||
List<String> words = seed.getMnemonicCode();
|
||||
setContent(getMnemonicWordsEntry(words.size()));
|
||||
setExpanded(true);
|
||||
|
||||
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
|
||||
wordEntry.getEditor().setText(words.get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected void enterMnemonic(int numWords) {
|
||||
setDescription("Generate new or enter existing");
|
||||
showHideLink.setVisible(false);
|
||||
@@ -231,7 +271,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
String text = change.getText();
|
||||
// if text was added, fix the text to fit the requirements
|
||||
if(!text.isEmpty()) {
|
||||
String newText = text.replace(" ", "").toLowerCase();
|
||||
String newText = text.replace(" ", "").toLowerCase(Locale.ROOT);
|
||||
int carretPos = change.getCaretPosition() - text.length() + newText.length();
|
||||
change.setText(newText);
|
||||
// fix caret position based on difference in originally added text and fixed text
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
public class NumberCell extends TreeTableCell<Entry, Number> {
|
||||
public NumberCell() {
|
||||
super();
|
||||
getStyleClass().add("number-cell");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
getStyleClass().add("number-field");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Number amount, boolean empty) {
|
||||
super.updateItem(amount, empty);
|
||||
|
||||
if(empty || amount == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(amount.toString());
|
||||
setGraphic(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.google.common.io.Files;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.crypto.BIP38;
|
||||
import com.sparrowwallet.drongo.crypto.DumpedPrivateKey;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
@@ -27,6 +28,7 @@ import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
@@ -80,7 +82,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
keyField.setText("Private Key:");
|
||||
key = new TextArea();
|
||||
key.setWrapText(true);
|
||||
key.setPromptText("Wallet Import Format (WIF)");
|
||||
key.setPromptText("Wallet Import Format (WIF) or BIP38 encrypted key");
|
||||
key.setPrefRowCount(2);
|
||||
key.getStyleClass().add("fixed-width");
|
||||
HBox keyBox = new HBox(5);
|
||||
@@ -149,6 +151,10 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
});
|
||||
|
||||
key.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(isEncryptedKey()) {
|
||||
decryptKey();
|
||||
}
|
||||
|
||||
boolean isValidKey = isValidKey();
|
||||
createButton.setDisable(!isValidKey || !isValidToAddress());
|
||||
if(isValidKey) {
|
||||
@@ -199,6 +205,27 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isEncryptedKey() {
|
||||
return key.getText().length() == 58 && key.getText().startsWith("6P");
|
||||
}
|
||||
|
||||
private void decryptKey() {
|
||||
PassphraseDialog passphraseDialog = new PassphraseDialog();
|
||||
Optional<String> optPassphrase = passphraseDialog.showAndWait();
|
||||
if(optPassphrase.isPresent()) {
|
||||
try {
|
||||
DumpedPrivateKey decryptedKey = BIP38.decrypt(optPassphrase.get(), key.getText());
|
||||
Platform.runLater(() -> key.setText(decryptedKey.toString()));
|
||||
} catch(Exception e) {
|
||||
log.error("Failed to decrypt BIP38 key", e);
|
||||
AppServices.showErrorDialog("Failed to decrypt BIP38 key", e.getMessage());
|
||||
Platform.runLater(() -> key.setText(""));
|
||||
}
|
||||
} else {
|
||||
Platform.runLater(() -> key.setText(""));
|
||||
}
|
||||
}
|
||||
|
||||
private DumpedPrivateKey getPrivateKey() {
|
||||
return DumpedPrivateKey.fromBase58(key.getText());
|
||||
}
|
||||
@@ -342,4 +369,35 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
glyph.setFontSize(12);
|
||||
return glyph;
|
||||
}
|
||||
|
||||
public class PassphraseDialog extends Dialog<String> {
|
||||
private final CustomPasswordField passphrase;
|
||||
|
||||
public PassphraseDialog() {
|
||||
this.passphrase = new ViewPasswordField();
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("BIP38 Passphrase");
|
||||
dialogPane.setHeaderText("Enter the BIP38 passphrase for this key:");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(200);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.KEY);
|
||||
key.setFontSize(50);
|
||||
dialogPane.setGraphic(key);
|
||||
|
||||
final VBox content = new VBox(10);
|
||||
content.setPrefHeight(50);
|
||||
content.getChildren().add(passphrase);
|
||||
|
||||
dialogPane.setContent(content);
|
||||
Platform.runLater(passphrase::requestFocus);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? passphrase.getText() : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class QRDisplayDialog extends Dialog<UR> {
|
||||
@@ -78,12 +79,7 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
if(encoder.isSinglePart()) {
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else {
|
||||
animateQRService = new AnimateQRService();
|
||||
animateQRService.setPeriod(Duration.millis(ANIMATION_PERIOD_MILLIS));
|
||||
animateQRService.start();
|
||||
setOnCloseRequest(event -> {
|
||||
animateQRService.cancel();
|
||||
});
|
||||
createAnimateQRService();
|
||||
}
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
@@ -124,10 +120,19 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
|
||||
}
|
||||
|
||||
private void createAnimateQRService() {
|
||||
animateQRService = new AnimateQRService();
|
||||
animateQRService.setPeriod(Duration.millis(ANIMATION_PERIOD_MILLIS));
|
||||
animateQRService.start();
|
||||
setOnCloseRequest(event -> {
|
||||
animateQRService.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
private void nextPart() {
|
||||
if(!useLegacyEncoding) {
|
||||
String fragment = encoder.nextPart();
|
||||
currentPart = fragment.toUpperCase();
|
||||
currentPart = fragment.toUpperCase(Locale.ROOT);
|
||||
} else {
|
||||
currentPart = legacyParts[legacyPartIndex];
|
||||
legacyPartIndex++;
|
||||
@@ -169,6 +174,8 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
|
||||
nextPart();
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else if(animateQRService == null) {
|
||||
createAnimateQRService();
|
||||
} else if(!animateQRService.isRunning()) {
|
||||
animateQRService.reset();
|
||||
animateQRService.start();
|
||||
@@ -185,6 +192,8 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
}
|
||||
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else if(animateQRService == null) {
|
||||
createAnimateQRService();
|
||||
} else if(!animateQRService.isRunning()) {
|
||||
animateQRService.reset();
|
||||
animateQRService.start();
|
||||
|
||||
@@ -10,15 +10,14 @@ import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||
import com.sparrowwallet.drongo.address.P2SHAddress;
|
||||
import com.sparrowwallet.drongo.address.P2WPKHAddress;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.Base43;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.SeedQR;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.hummingbird.LegacyURDecoder;
|
||||
import com.sparrowwallet.hummingbird.registry.*;
|
||||
@@ -186,7 +185,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
String qrtext = qrResult.getText();
|
||||
Matcher partMatcher = PART_PATTERN.matcher(qrtext);
|
||||
|
||||
if(qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
|
||||
if(qrtext.toLowerCase(Locale.ROOT).startsWith(UR.UR_PREFIX)) {
|
||||
if(LegacyURDecoder.isLegacyURFragment(qrtext)) {
|
||||
legacyDecoder.receivePart(qrtext);
|
||||
Platform.runLater(() -> percentComplete.setValue(legacyDecoder.getPercentComplete()));
|
||||
@@ -257,6 +256,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
BitcoinURI bitcoinURI;
|
||||
Address address;
|
||||
ExtendedKey extendedKey;
|
||||
DeterministicSeed seed;
|
||||
try {
|
||||
extendedKey = ExtendedKey.fromDescriptor(qrtext);
|
||||
result = new Result(extendedKey);
|
||||
@@ -334,6 +334,22 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
//Ignore, not parseable as base43 decoded bytes
|
||||
}
|
||||
|
||||
try {
|
||||
seed = SeedQR.getSeed(qrtext);
|
||||
result = new Result(seed);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//Ignore, not parseable as a SeedQR
|
||||
}
|
||||
|
||||
try {
|
||||
seed = SeedQR.getSeed(qrResult.getRawBytes());
|
||||
result = new Result(seed);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//Ignore, not parseable as a CompactSeedQR
|
||||
}
|
||||
|
||||
result = new Result(qrtext);
|
||||
}
|
||||
}
|
||||
@@ -401,6 +417,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
CryptoAccount cryptoAccount = (CryptoAccount)ur.decodeFromRegistry();
|
||||
List<Wallet> wallets = getWallets(cryptoAccount);
|
||||
return new Result(wallets);
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_SEED)) {
|
||||
CryptoSeed cryptoSeed = (CryptoSeed)ur.decodeFromRegistry();
|
||||
DeterministicSeed seed = getSeed(cryptoSeed);
|
||||
return new Result(seed);
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_BIP39)) {
|
||||
CryptoBip39 cryptoBip39 = (CryptoBip39)ur.decodeFromRegistry();
|
||||
DeterministicSeed seed = getSeed(cryptoBip39);
|
||||
return new Result(seed);
|
||||
} else {
|
||||
log.error("Unsupported UR type " + urRegistryType);
|
||||
return new Result(new URException("UR type " + urRegistryType + " is not supported"));
|
||||
@@ -523,6 +547,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private DeterministicSeed getSeed(CryptoSeed cryptoSeed) {
|
||||
return new DeterministicSeed(cryptoSeed.getSeed(), null, cryptoSeed.getBirthdate() == null ? System.currentTimeMillis() : cryptoSeed.getBirthdate().getTime());
|
||||
}
|
||||
|
||||
private DeterministicSeed getSeed(CryptoBip39 cryptoBip39) {
|
||||
return new DeterministicSeed(cryptoBip39.getWords(), null, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
|
||||
}
|
||||
}
|
||||
|
||||
private class QRScanListener implements WebcamListener {
|
||||
@@ -626,6 +658,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
public final ExtendedKey extendedKey;
|
||||
public final OutputDescriptor outputDescriptor;
|
||||
public final List<Wallet> wallets;
|
||||
public final DeterministicSeed seed;
|
||||
public final String payload;
|
||||
public final Throwable exception;
|
||||
|
||||
@@ -636,6 +669,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -647,6 +681,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -658,6 +693,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -669,6 +705,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -680,6 +717,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = extendedKey;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -691,6 +729,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -702,6 +741,19 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = wallets;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
public Result(DeterministicSeed seed) {
|
||||
this.transaction = null;
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = seed;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -713,6 +765,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = payload;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -724,6 +777,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import tornadofx.control.Form;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SearchWalletDialog extends Dialog<Entry> {
|
||||
private static final Logger log = LoggerFactory.getLogger(SearchWalletDialog.class);
|
||||
@@ -134,7 +135,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
});
|
||||
|
||||
search.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
searchWallet(newValue.toLowerCase());
|
||||
searchWallet(newValue.toLowerCase(Locale.ROOT));
|
||||
});
|
||||
|
||||
setResizable(true);
|
||||
@@ -159,7 +160,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase().contains(searchText)) ||
|
||||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
@@ -170,8 +171,8 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase().contains(searchText)) ||
|
||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
@@ -184,8 +185,8 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase().contains(searchText)) ||
|
||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
@@ -197,8 +198,8 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
if(hashIndexEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase().contains(searchText)) ||
|
||||
if(hashIndexEntry.getBlockTransaction().getHash().toString().toLowerCase(Locale.ROOT).equals(searchText) ||
|
||||
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.StringConverter;
|
||||
@@ -47,7 +48,19 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
|
||||
Grid grid = getGrid(initialPayments);
|
||||
|
||||
spreadsheetView = new SpreadsheetView(grid);
|
||||
spreadsheetView = new SpreadsheetView(grid) {
|
||||
@Override
|
||||
public void pasteClipboard() {
|
||||
final Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||
if(clipboard.hasString()) {
|
||||
final TablePosition<?,?> tp = getSelectionModel().getFocusedCell();
|
||||
SpreadsheetCell cell = getGrid().getRows().get(tp.getRow()).get(tp.getColumn());
|
||||
getGrid().setCellValue(cell.getRow(), cell.getColumn(), cell.getCellType().convertValue(clipboard.getString()));
|
||||
} else {
|
||||
super.pasteClipboard();
|
||||
}
|
||||
}
|
||||
};
|
||||
spreadsheetView.getColumns().get(0).setPrefWidth(400);
|
||||
spreadsheetView.getColumns().get(1).setPrefWidth(150);
|
||||
spreadsheetView.getColumns().get(2).setPrefWidth(247);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
@@ -26,10 +27,10 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
@@ -47,6 +48,8 @@ import org.controlsfx.tools.Platform;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.control.CoinLabel.BTC_FORMAT;
|
||||
|
||||
public class TransactionDiagram extends GridPane {
|
||||
private static final int MAX_UTXOS = 8;
|
||||
private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 2;
|
||||
@@ -70,7 +73,7 @@ public class TransactionDiagram extends GridPane {
|
||||
private final EventHandler<MouseEvent> expandedDiagramHandler = new EventHandler<>() {
|
||||
@Override
|
||||
public void handle(MouseEvent event) {
|
||||
if(!event.isConsumed()) {
|
||||
if(!event.isConsumed() && event.getButton() != MouseButton.SECONDARY) {
|
||||
Stage stage = new Stage(StageStyle.UNDECORATED);
|
||||
stage.setTitle(walletTx.getPayments().iterator().next().getLabel());
|
||||
stage.initOwner(TransactionDiagram.this.getScene().getWindow());
|
||||
@@ -420,6 +423,9 @@ public class TransactionDiagram extends GridPane {
|
||||
label.setGraphic(excludeUtxoButton);
|
||||
label.setContentDisplay(ContentDisplay.LEFT);
|
||||
}
|
||||
|
||||
ContextMenu contextMenu = new LabelContextMenu(walletNode.getAddress(), inputValue);
|
||||
label.setContextMenu(contextMenu);
|
||||
} else {
|
||||
if(input instanceof PayjoinBlockTransactionHashIndex) {
|
||||
tooltip.setText("Added once transaction is signed and sent to the payjoin server");
|
||||
@@ -448,6 +454,9 @@ public class TransactionDiagram extends GridPane {
|
||||
Address fromAddress = txOutput.getScript().getToAddress();
|
||||
inputValue = txOutput.getValue();
|
||||
tooltip.setText("Input of " + getSatsValue(inputValue) + " sats\n" + input.getHashAsString() + ":" + input.getIndex() + (fromAddress != null ? "\n" + fromAddress : ""));
|
||||
|
||||
ContextMenu contextMenu = new LabelContextMenu(fromAddress, inputValue);
|
||||
label.setContextMenu(contextMenu);
|
||||
} else {
|
||||
tooltip.setText(input.getHashAsString() + ":" + input.getIndex());
|
||||
}
|
||||
@@ -650,7 +659,7 @@ public class TransactionDiagram extends GridPane {
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
Wallet toWallet = getToWallet(payment);
|
||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null;
|
||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
||||
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
||||
+ getSatsValue(payment.getAmount()) + " sats to "
|
||||
@@ -731,6 +740,11 @@ public class TransactionDiagram extends GridPane {
|
||||
for(OutputNode outputNode : outputNodes) {
|
||||
outputsBox.getChildren().add(outputNode.outputLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount);
|
||||
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
|
||||
outputLabelControl.setContextMenu(contextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
boolean highFee = (walletTx.getFeePercentage() > 0.1);
|
||||
@@ -759,6 +773,11 @@ public class TransactionDiagram extends GridPane {
|
||||
outputsBox.getChildren().add(feeBox);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
if(walletTx.getFee() >= 0) {
|
||||
ContextMenu contextMenu = new LabelContextMenu(null, walletTx.getFee());
|
||||
feeLabel.setContextMenu(contextMenu);
|
||||
}
|
||||
|
||||
return outputsBox;
|
||||
}
|
||||
|
||||
@@ -1194,11 +1213,11 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
private class OutputNode implements Comparable<OutputNode> {
|
||||
public Node outputLabel;
|
||||
public Pane outputLabel;
|
||||
public Address address;
|
||||
public long amount;
|
||||
|
||||
public OutputNode(Node outputLabel, Address address, long amount) {
|
||||
public OutputNode(Pane outputLabel, Address address, long amount) {
|
||||
this.outputLabel = outputLabel;
|
||||
this.address = address;
|
||||
this.amount = amount;
|
||||
@@ -1213,4 +1232,35 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class LabelContextMenu extends ContextMenu {
|
||||
public LabelContextMenu(Address address, long value) {
|
||||
if(address != null) {
|
||||
MenuItem copyAddress = new MenuItem("Copy Address");
|
||||
copyAddress.setOnAction(event -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(address.toString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyAddress);
|
||||
}
|
||||
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||
copySatsValue.setOnAction(event -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(Long.toString(value));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
MenuItem copyBtcValue = new MenuItem("Copy Value in BTC");
|
||||
copyBtcValue.setOnAction(event -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(BTC_FORMAT.format((double)value / Transaction.SATOSHIS_PER_BITCOIN));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().addAll(copySatsValue, copyBtcValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.TableColumnBase;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
|
||||
@@ -24,6 +25,7 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
});
|
||||
dateCol.setCellFactory(p -> new DateCell());
|
||||
dateCol.setSortable(true);
|
||||
dateCol.setComparator(dateCol.getComparator().reversed());
|
||||
getColumns().add(dateCol);
|
||||
|
||||
TreeTableColumn<Entry, Entry> outputCol = new TreeTableColumn<>("Output");
|
||||
@@ -35,7 +37,12 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
outputCol.setComparator((o1, o2) -> {
|
||||
UtxoEntry entry1 = (UtxoEntry)o1;
|
||||
UtxoEntry entry2 = (UtxoEntry)o2;
|
||||
return entry1.getDescription().compareTo(entry2.getDescription());
|
||||
int hashCompare = entry1.getHashIndex().getHash().toString().compareTo(entry2.getHashIndex().getHash().toString());
|
||||
if(hashCompare != 0) {
|
||||
return hashCompare;
|
||||
}
|
||||
|
||||
return (int)(entry1.getHashIndex().getIndex() - entry2.getHashIndex().getIndex());
|
||||
});
|
||||
getColumns().add(outputCol);
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class WalletLabelDialog extends Dialog<String> {
|
||||
private static final int MAX_LABEL_LENGTH = 25;
|
||||
|
||||
@@ -29,7 +31,7 @@ public class WalletLabelDialog extends Dialog<String> {
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle(walletType + " Name");
|
||||
dialogPane.setHeaderText("Enter a name for this " + walletType.toLowerCase() + ":");
|
||||
dialogPane.setHeaderText("Enter a name for this " + walletType.toLowerCase(Locale.ROOT) + ":");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(400);
|
||||
|
||||
@@ -84,7 +84,7 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||
okButton.setPrefWidth(130);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> passwordConfirm.isVisible() && !password.getText().equals(passwordConfirm.getText()), password.textProperty(), passwordConfirm.textProperty());
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> (requirement == PasswordRequirement.LOAD && password.getText().isEmpty()) || (passwordConfirm.isVisible() && !password.getText().equals(passwordConfirm.getText())), password.textProperty(), passwordConfirm.textProperty());
|
||||
okButton.disableProperty().bind(isInvalid);
|
||||
|
||||
if(requirement != PasswordRequirement.UPDATE_NEW && requirement != PasswordRequirement.UPDATE_CHANGE) {
|
||||
|
||||
@@ -8,10 +8,16 @@ import java.util.List;
|
||||
|
||||
public class SendActionEvent extends FunctionActionEvent {
|
||||
private final List<BlockTransactionHashIndex> utxos;
|
||||
private final boolean selectIfEmpty;
|
||||
|
||||
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
this(wallet, utxos, false);
|
||||
}
|
||||
|
||||
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, boolean selectIfEmpty) {
|
||||
super(Function.SEND, wallet);
|
||||
this.utxos = utxos;
|
||||
this.selectIfEmpty = selectIfEmpty;
|
||||
}
|
||||
|
||||
public List<BlockTransactionHashIndex> getUtxos() {
|
||||
@@ -20,6 +26,6 @@ public class SendActionEvent extends FunctionActionEvent {
|
||||
|
||||
@Override
|
||||
public boolean selectFunction() {
|
||||
return !getUtxos().isEmpty();
|
||||
return selectIfEmpty || !getUtxos().isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class ShowTransactionsCountEvent {
|
||||
private final boolean showCount;
|
||||
|
||||
public ShowTransactionsCountEvent(boolean showCount) {
|
||||
this.showCount = showCount;
|
||||
}
|
||||
|
||||
public boolean isShowCount() {
|
||||
return showCount;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
@@ -7,10 +8,19 @@ import javafx.stage.Window;
|
||||
|
||||
public class ViewTransactionEvent {
|
||||
private final Window window;
|
||||
private final Transaction transaction;
|
||||
private final BlockTransaction blockTransaction;
|
||||
private final TransactionView initialView;
|
||||
private final Integer initialIndex;
|
||||
|
||||
public ViewTransactionEvent(Window window, Transaction transaction) {
|
||||
this.window = window;
|
||||
this.transaction = transaction;
|
||||
this.blockTransaction = null;
|
||||
this.initialView = TransactionView.HEADERS;
|
||||
this.initialIndex = null;
|
||||
}
|
||||
|
||||
public ViewTransactionEvent(Window window, BlockTransaction blockTransaction) {
|
||||
this(window, blockTransaction, TransactionView.HEADERS, null);
|
||||
}
|
||||
@@ -21,6 +31,7 @@ public class ViewTransactionEvent {
|
||||
|
||||
public ViewTransactionEvent(Window window, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||
this.window = window;
|
||||
this.transaction = blockTransaction.getTransaction();
|
||||
this.blockTransaction = blockTransaction;
|
||||
this.initialView = initialView;
|
||||
this.initialIndex = initialIndex;
|
||||
@@ -30,6 +41,10 @@ public class ViewTransactionEvent {
|
||||
return window;
|
||||
}
|
||||
|
||||
public Transaction getTransaction() {
|
||||
return transaction;
|
||||
}
|
||||
|
||||
public BlockTransaction getBlockTransaction() {
|
||||
return blockTransaction;
|
||||
}
|
||||
|
||||
@@ -45,10 +45,10 @@ public class WalletHistoryChangedEvent extends WalletChangedEvent {
|
||||
}
|
||||
|
||||
public List<WalletNode> getReceiveNodes() {
|
||||
return getWallet().getNode(KeyPurpose.RECEIVE).getChildren().stream().filter(historyChangedNodes::contains).collect(Collectors.toList());
|
||||
return historyChangedNodes.stream().filter(node -> node.getKeyPurpose() == KeyPurpose.RECEIVE).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public List<WalletNode> getChangeNodes() {
|
||||
return getWallet().getNode(KeyPurpose.CHANGE).getChildren().stream().filter(historyChangedNodes::contains).collect(Collectors.toList());
|
||||
return historyChangedNodes.stream().filter(node -> node.getKeyPurpose() == KeyPurpose.CHANGE).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,12 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
ScriptType scriptType = ScriptType.valueOf(cf.addressType.replace('-', '_'));
|
||||
|
||||
for(ExtPublicKey extKey : cf.extendedPublicKeys) {
|
||||
Keystore keystore = new Keystore(extKey.name);
|
||||
Keystore keystore = new Keystore(extKey.name.length() > Keystore.MAX_LABEL_LENGTH ? extKey.name.substring(0, Keystore.MAX_LABEL_LENGTH) : extKey.name);
|
||||
|
||||
if("Unknown".equals(extKey.bip32Path)) {
|
||||
extKey.bip32Path = "m/45'/0/0/0";
|
||||
}
|
||||
|
||||
try {
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path));
|
||||
} catch(NumberFormatException e) {
|
||||
@@ -70,6 +75,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
keystore.setWalletModel(walletModel);
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
}
|
||||
wallet.makeLabelsUnique(keystore);
|
||||
wallet.getKeystores().add(keystore);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ public class CoboVaultMultisig extends ColdcardMultisig {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file or QR created by using the Multisig Wallet > ... > Show/Export XPUB feature on your Cobo Vault.";
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
|
||||
public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
private static final Logger log = LoggerFactory.getLogger(CoboVaultSinglesig.class);
|
||||
@@ -24,7 +25,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file or QR created by using the My Cobo Vault > ... > Export Wallet feature on your Cobo Vault.";
|
||||
}
|
||||
|
||||
@@ -47,7 +48,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COBO_VAULT);
|
||||
keystore.setKeyDerivation(new KeyDerivation(coboKeystore.MasterFingerprint.toLowerCase(), "m/" + coboKeystore.AccountKeyPath));
|
||||
keystore.setKeyDerivation(new KeyDerivation(coboKeystore.MasterFingerprint.toLowerCase(Locale.ROOT), "m/" + coboKeystore.AccountKeyPath));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(coboKeystore.ExtPubKey));
|
||||
|
||||
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(coboKeystore.ExtPubKey);
|
||||
|
||||
@@ -18,6 +18,7 @@ import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class ColdcardMultisig implements WalletImport, KeystoreFileImport, WalletExport {
|
||||
@@ -84,8 +85,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import file created by using the Settings > Multisig Wallets > Export XPUB > 0 feature on your Coldcard.";
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file created by using the Settings > Multisig Wallets > Export XPUB > " + account + " feature on your Coldcard.";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -198,7 +199,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
if(multipleDerivations) {
|
||||
writer.append("Derivation: ").append(keystore.getKeyDerivation().getDerivationPath()).append("\n");
|
||||
}
|
||||
writer.append(keystore.getKeyDerivation().getMasterFingerprint().toUpperCase()).append(": ").append(keystore.getExtendedPublicKey().toString()).append("\n");
|
||||
writer.append(keystore.getKeyDerivation().getMasterFingerprint().toUpperCase(Locale.ROOT)).append(": ").append(keystore.getExtendedPublicKey().toString()).append("\n");
|
||||
if(multipleDerivations) {
|
||||
writer.append("\n");
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
@@ -28,8 +29,8 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON > 0 feature on your Coldcard. Note this requires firmware version 3.1.3 or later.";
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON > " + account + " feature on your Coldcard. Note this requires firmware version 3.1.3 or later.";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -71,7 +72,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
ColdcardKeystore ck = gson.fromJson(map.get(key), ColdcardKeystore.class);
|
||||
|
||||
if(ck.name != null) {
|
||||
ScriptType ckScriptType = ScriptType.valueOf(ck.name.replace("p2wpkh-p2sh", "p2sh_p2wpkh").replace("p2sh-p2wpkh", "p2sh_p2wpkh").toUpperCase());
|
||||
ScriptType ckScriptType = ScriptType.valueOf(ck.name.replace("p2wpkh-p2sh", "p2sh_p2wpkh").replace("p2sh-p2wpkh", "p2sh_p2wpkh").toUpperCase(Locale.ROOT));
|
||||
if(ckScriptType.equals(scriptType)) {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(getName());
|
||||
|
||||
@@ -15,8 +15,10 @@ import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.ENUMERATE_HW_PERIOD_SECS;
|
||||
import static com.sparrowwallet.sparrow.net.PagedBatchRequestBuilder.DEFAULT_PAGE_SIZE;
|
||||
import static com.sparrowwallet.sparrow.net.TcpTransport.DEFAULT_MAX_TIMEOUT;
|
||||
import static com.sparrowwallet.sparrow.wallet.WalletUtxosEntry.DUST_ATTACK_THRESHOLD_SATS;
|
||||
|
||||
public class Config {
|
||||
private static final Logger log = LoggerFactory.getLogger(Config.class);
|
||||
@@ -41,10 +43,13 @@ public class Config {
|
||||
private boolean hideEmptyUsedAddresses = false;
|
||||
private boolean showTransactionHex = true;
|
||||
private boolean showLoadingLog = true;
|
||||
private boolean showAddressTransactionCount = false;
|
||||
private boolean preventSleep = false;
|
||||
private List<File> recentWalletFiles;
|
||||
private Integer keyDerivationPeriod;
|
||||
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
|
||||
private File hwi;
|
||||
private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS;
|
||||
private Boolean hdCapture;
|
||||
private String webcamDevice;
|
||||
private ServerType serverType;
|
||||
@@ -274,6 +279,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isShowAddressTransactionCount() {
|
||||
return showAddressTransactionCount;
|
||||
}
|
||||
|
||||
public void setShowAddressTransactionCount(boolean showAddressTransactionCount) {
|
||||
this.showAddressTransactionCount = showAddressTransactionCount;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isPreventSleep() {
|
||||
return preventSleep;
|
||||
}
|
||||
@@ -300,6 +314,10 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public long getDustAttackThreshold() {
|
||||
return dustAttackThreshold;
|
||||
}
|
||||
|
||||
public File getHwi() {
|
||||
return hwi;
|
||||
}
|
||||
@@ -309,6 +327,10 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public int getEnumerateHwPeriod() {
|
||||
return enumerateHwPeriod;
|
||||
}
|
||||
|
||||
public Boolean getHdCapture() {
|
||||
return hdCapture;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import a single keystore from an Electrum wallet (use File > Import > Electrum to import a multisig wallet).";
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
if(ek.root_fingerprint == null && ek.ckcc_xfp != null) {
|
||||
byte[] le = new byte[4];
|
||||
Utils.uint32ToByteArrayLE(Long.parseLong(ek.ckcc_xfp), le, 0);
|
||||
ek.root_fingerprint = Utils.bytesToHex(le).toUpperCase();
|
||||
ek.root_fingerprint = Utils.bytesToHex(le).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
ew.keystores.put(key, ek);
|
||||
}
|
||||
@@ -95,6 +95,10 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
}
|
||||
}
|
||||
|
||||
if(key.equals("gap_limit") && map.get(key) instanceof JsonPrimitive gapLimit) {
|
||||
ew.gap_limit = gapLimit.getAsInt();
|
||||
}
|
||||
|
||||
if(key.equals("addresses")) {
|
||||
ew.addresses = gson.fromJson(map.get(key), ElectrumAddresses.class);
|
||||
}
|
||||
@@ -209,6 +213,10 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
throw new ImportException("Unknown Electrum wallet type of " + ew.wallet_type);
|
||||
}
|
||||
|
||||
if(ew.gap_limit != null) {
|
||||
wallet.setGapLimit(ew.gap_limit);
|
||||
}
|
||||
|
||||
for(String key : ew.labels.keySet()) {
|
||||
try {
|
||||
Sha256Hash txHash = Sha256Hash.wrap(key);
|
||||
@@ -408,6 +416,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
public String seed_type;
|
||||
public Boolean use_encryption;
|
||||
public ElectrumAddresses addresses;
|
||||
public Integer gap_limit;
|
||||
public Map<String, String> labels = new LinkedHashMap<>();
|
||||
public Map<Sha256Hash, BlockTransaction> transactions = new HashMap<>();
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public class GordianSeedTool implements KeystoreFileImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Select your seed and scan the QR code created by Authenticate > Derive Key > Other Key Derivations > " + Network.get().toDisplayString() + " > Master Key > Account Descriptor. Click the share icon at the bottom.";
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
@@ -33,7 +34,7 @@ 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.0.2";
|
||||
private static final String HWI_VERSION = "2.1.1";
|
||||
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
|
||||
|
||||
private static boolean isPromptActive = false;
|
||||
@@ -240,32 +241,44 @@ public class Hwi {
|
||||
}
|
||||
|
||||
private String execute(List<String> command) throws IOException {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||
Process process = processBuilder.start();
|
||||
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
return CharStreams.toString(reader);
|
||||
long start = System.currentTimeMillis();
|
||||
Process process = null;
|
||||
try {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||
process = processBuilder.start();
|
||||
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
return CharStreams.toString(reader);
|
||||
}
|
||||
} finally {
|
||||
deleteExtractionOnFailure(process, start);
|
||||
}
|
||||
}
|
||||
|
||||
private String execute(List<String> arguments, Command command, String... commandArguments) throws IOException {
|
||||
List<String> processArguments = new ArrayList<>(arguments);
|
||||
processArguments.add("--stdin");
|
||||
long start = System.currentTimeMillis();
|
||||
Process process = null;
|
||||
try {
|
||||
List<String> processArguments = new ArrayList<>(arguments);
|
||||
processArguments.add("--stdin");
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(processArguments);
|
||||
Process process = processBuilder.start();
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(processArguments);
|
||||
process = processBuilder.start();
|
||||
|
||||
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
|
||||
writer.write(command.toString());
|
||||
for(String commandArgument : commandArguments) {
|
||||
writer.write(" \"");
|
||||
writer.write(commandArgument.replace("\\", "\\\\").replace("\"", "\\\""));
|
||||
writer.write("\"");
|
||||
try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8))) {
|
||||
writer.write(command.toString());
|
||||
for(String commandArgument : commandArguments) {
|
||||
writer.write(" \"");
|
||||
writer.write(commandArgument.replace("\\", "\\\\").replace("\"", "\\\""));
|
||||
writer.write("\"");
|
||||
}
|
||||
writer.flush();
|
||||
}
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
return CharStreams.toString(reader);
|
||||
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
return CharStreams.toString(reader);
|
||||
}
|
||||
} finally {
|
||||
deleteExtractionOnFailure(process, start);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +307,14 @@ public class Hwi {
|
||||
//The check will still happen on first invocation, but will not thereafter
|
||||
//See https://github.com/bitcoin-core/HWI/issues/327 for details
|
||||
if(platform == Platform.OSX) {
|
||||
InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.zip");
|
||||
String osArch = System.getProperty("os.arch");
|
||||
InputStream inputStream;
|
||||
if(osArch.equals("aarch64")) {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/osx/aarch64/" + HWI_VERSION_DIR + "-mac-aarch64-signed.zip");
|
||||
} else {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.zip");
|
||||
}
|
||||
|
||||
if(inputStream == null) {
|
||||
throw new IllegalStateException("Cannot load " + HWI_VERSION_DIR + " from classpath");
|
||||
}
|
||||
@@ -336,8 +356,9 @@ public class Hwi {
|
||||
InputStream inputStream;
|
||||
Path tempExecPath;
|
||||
if(platform == Platform.WINDOWS) {
|
||||
Files.createDirectories(getHwiHomeDir().toPath());
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
|
||||
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null);
|
||||
tempExecPath = Files.createTempFile(getHwiHomeDir().toPath(), HWI_VERSION_DIR, null);
|
||||
} else {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
|
||||
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
@@ -368,7 +389,7 @@ public class Hwi {
|
||||
}
|
||||
|
||||
private File getHwiHomeDir() {
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
if(Platform.getCurrent() == Platform.OSX || Platform.getCurrent() == Platform.WINDOWS) {
|
||||
return new File(Storage.getSparrowDir(), HWI_HOME_DIR);
|
||||
}
|
||||
|
||||
@@ -424,6 +445,40 @@ public class Hwi {
|
||||
return result.get("success").getAsBoolean();
|
||||
}
|
||||
|
||||
private void deleteExtractionOnFailure(Process process, long after) {
|
||||
try {
|
||||
if(Platform.getCurrent() != Platform.OSX && process != null && process.waitFor(100, TimeUnit.MILLISECONDS) && process.exitValue() != 0) {
|
||||
File extraction = getTemporaryExtraction(after);
|
||||
if(extraction != null) {
|
||||
IOUtils.deleteDirectory(extraction);
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error deleting temporary extraction", e);
|
||||
}
|
||||
}
|
||||
|
||||
private File getTemporaryExtraction(long after) {
|
||||
File tmpDir = new File(System.getProperty("java.io.tmpdir"));
|
||||
if(!tmpDir.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File[] tmps = tmpDir.listFiles(file -> {
|
||||
if(!file.isDirectory() || file.lastModified() < after) {
|
||||
return false;
|
||||
}
|
||||
String name = file.getName();
|
||||
if(name.length() < 9 || !name.startsWith("_MEI")) {
|
||||
return false;
|
||||
}
|
||||
File hwilib = new File(file, "hwilib");
|
||||
return hwilib.exists();
|
||||
});
|
||||
|
||||
return tmps == null || tmps.length == 0 ? null : Arrays.stream(tmps).sorted(Comparator.comparingLong(File::lastModified)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
private List<String> getDeviceArguments(Device device, Command command) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType()));
|
||||
addChainType(elements, false);
|
||||
@@ -702,7 +757,7 @@ public class Hwi {
|
||||
private static class DeviceModelSerializer implements JsonSerializer<WalletModel> {
|
||||
@Override
|
||||
public JsonElement serialize(WalletModel src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.toString().toLowerCase());
|
||||
return new JsonPrimitive(src.toString().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,7 +766,7 @@ public class Hwi {
|
||||
public WalletModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
String modelStr = json.getAsJsonPrimitive().getAsString();
|
||||
try {
|
||||
return WalletModel.valueOf(modelStr.toUpperCase());
|
||||
return WalletModel.valueOf(modelStr.toUpperCase(Locale.ROOT));
|
||||
} catch(Exception e) {
|
||||
for(WalletModel model : WalletModel.values()) {
|
||||
if(modelStr.startsWith(model.getType())) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
@@ -9,16 +12,19 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
public class IOUtils {
|
||||
private static final Logger log = LoggerFactory.getLogger(IOUtils.class);
|
||||
|
||||
public static FileType getFileType(File file) {
|
||||
try {
|
||||
String type = Files.probeContentType(file.toPath());
|
||||
if(type == null) {
|
||||
if(file.getName().toLowerCase().endsWith("txn") || file.getName().toLowerCase().endsWith("psbt")) {
|
||||
if(file.getName().toLowerCase(Locale.ROOT).endsWith("txn") || file.getName().toLowerCase(Locale.ROOT).endsWith("psbt")) {
|
||||
return FileType.TEXT;
|
||||
}
|
||||
|
||||
@@ -120,4 +126,28 @@ public class IOUtils {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean secureDelete(File file) {
|
||||
if(file.exists()) {
|
||||
long length = file.length();
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] data = new byte[1024*1024];
|
||||
random.nextBytes(data);
|
||||
try(RandomAccessFile raf = new RandomAccessFile(file, "rws")) {
|
||||
raf.seek(0);
|
||||
raf.getFilePointer();
|
||||
int pos = 0;
|
||||
while(pos < length) {
|
||||
raf.write(data);
|
||||
pos += data.length;
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.warn("Error overwriting file for deletion " + file.getName(), e);
|
||||
}
|
||||
|
||||
return file.delete();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.io;
|
||||
import com.google.gson.*;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.crypto.Argon2KeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.AsymmetricKeyDeriver;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
@@ -305,6 +307,11 @@ public class JsonPersistence implements Persistence {
|
||||
com.google.common.io.Files.copy(walletFile, outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
//Nothing required
|
||||
@@ -326,6 +333,8 @@ public class JsonPersistence implements Persistence {
|
||||
gsonBuilder.registerTypeAdapter(Date.class, new DateDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionSerializer());
|
||||
gsonBuilder.registerTypeAdapter(Transaction.class, new TransactionDeserializer());
|
||||
gsonBuilder.registerTypeAdapter(Address.class, new AddressSerializer());
|
||||
gsonBuilder.registerTypeAdapter(Address.class, new AddressDeserializer());
|
||||
if(includeWalletSerializers) {
|
||||
gsonBuilder.registerTypeAdapter(Keystore.class, new KeystoreSerializer());
|
||||
gsonBuilder.registerTypeAdapter(WalletNode.class, new NodeSerializer());
|
||||
@@ -424,6 +433,24 @@ public class JsonPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddressSerializer implements JsonSerializer<Address> {
|
||||
@Override
|
||||
public JsonElement serialize(Address src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddressDeserializer implements JsonDeserializer<Address> {
|
||||
@Override
|
||||
public Address deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
try {
|
||||
return Address.fromString(json.getAsJsonPrimitive().getAsString());
|
||||
} catch(InvalidAddressException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class KeystoreSerializer implements JsonSerializer<Keystore> {
|
||||
@Override
|
||||
public JsonElement serialize(Keystore keystore, Type typeOfSrc, JsonSerializationContext context) {
|
||||
|
||||
@@ -28,7 +28,7 @@ public class KeystoneMultisig extends ColdcardMultisig {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file or QR created by using the Multisig Wallet > ... > Show/Export XPUB feature on your Keystone.";
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file or QR created by using the My Keystone > ... > Export Wallet feature on your Keystone. Make sure to set the Watch-only Wallet to Sparrow in the Settings first.";
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ import java.io.InputStream;
|
||||
|
||||
public interface KeystoreFileImport extends KeystoreImport, FileImport {
|
||||
Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
|
||||
String getKeystoreImportDescription(int account);
|
||||
default String getKeystoreImportDescription() {
|
||||
return getKeystoreImportDescription(0);
|
||||
}
|
||||
boolean isKeystoreImportScannable();
|
||||
default boolean isFileFormatAvailable() {
|
||||
return true;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
public interface KeystoreImport extends Import {
|
||||
String getKeystoreImportDescription();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public class PassportMultisig extends ColdcardMultisig {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
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.";
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ public class PassportSinglesig extends ColdcardSinglesig {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
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.";
|
||||
}
|
||||
|
||||
|
||||
@@ -25,5 +25,6 @@ public interface Persistence {
|
||||
String getWalletId(Storage storage, Wallet wallet);
|
||||
String getWalletName(File walletFile, Wallet wallet);
|
||||
void copyWallet(File walletFile, OutputStream outputStream) throws IOException;
|
||||
boolean isClosed();
|
||||
void close();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ public class SeedSigner extends SpecterDIY {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import QR created on your SeedSigner by selecting xPub from Seed in the Seed Tools menu once you have entered your seed.";
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file or QR created by using the Master Public Keys feature on your Specter DIY device. Note the default is P2WPKH for Single Signature, and P2WSH for Multi Signature.";
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
if(walletModel != null) {
|
||||
keystore.setWalletModel(walletModel);
|
||||
if(walletModel == WalletModel.TREZOR_1 || walletModel == WalletModel.TREZOR_T || walletModel == WalletModel.KEEPKEY ||
|
||||
walletModel == WalletModel.LEDGER_NANO_S || walletModel == WalletModel.LEDGER_NANO_X ||
|
||||
walletModel == WalletModel.LEDGER_NANO_S || walletModel == WalletModel.LEDGER_NANO_X || walletModel == WalletModel.LEDGER_NANO_S_PLUS ||
|
||||
walletModel == WalletModel.BITBOX_02 || walletModel == WalletModel.COLDCARD) {
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
} else if(walletModel == WalletModel.BITCOIN_CORE) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.MainApp;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
@@ -126,6 +127,10 @@ public class Storage {
|
||||
return persistence.isPersisted(this, wallet);
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return persistence.isClosed();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
ClosePersistenceService closePersistenceService = new ClosePersistenceService();
|
||||
closePersistenceService.start();
|
||||
@@ -163,6 +168,11 @@ public class Storage {
|
||||
persistence.copyWallet(walletFile, outputStream);
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
deleteBackups();
|
||||
IOUtils.secureDelete(walletFile);
|
||||
}
|
||||
|
||||
public void deleteBackups() {
|
||||
deleteBackups(null);
|
||||
}
|
||||
@@ -181,7 +191,7 @@ public class Storage {
|
||||
private void deleteBackups(String prefix) {
|
||||
File[] backups = getBackups(prefix);
|
||||
for(File backup : backups) {
|
||||
backup.delete();
|
||||
IOUtils.secureDelete(backup);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,6 +300,10 @@ public class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
if(AppServices.get().getOpenWallets().keySet().stream().anyMatch(wallet -> walletName.equals(wallet.getName()))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return RESERVED_WALLET_NAMES.contains(walletName);
|
||||
}
|
||||
|
||||
|
||||
@@ -84,9 +84,13 @@ public class DbPersistence implements Persistence {
|
||||
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
|
||||
masterWallet = jdbi.withHandle(handle -> {
|
||||
WalletDao walletDao = handle.attach(WalletDao.class);
|
||||
return walletDao.getMainWallet(MASTER_SCHEMA);
|
||||
return walletDao.getMainWallet(MASTER_SCHEMA, getWalletName(storage.getWalletFile(), null));
|
||||
});
|
||||
|
||||
if(masterWallet == null) {
|
||||
throw new StorageException("The wallet file was corrupted. Check the backups folder for previous copies.");
|
||||
}
|
||||
|
||||
Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, masterWallet, encryptionKey);
|
||||
masterWallet.setChildWallets(childWallets.keySet().stream().map(WalletAndKey::getWallet).collect(Collectors.toList()));
|
||||
|
||||
@@ -109,7 +113,7 @@ public class DbPersistence implements Persistence {
|
||||
Jdbi childJdbi = getJdbi(storage, getFilePassword(encryptionKey));
|
||||
Wallet wallet = childJdbi.withHandle(handle -> {
|
||||
WalletDao walletDao = handle.attach(WalletDao.class);
|
||||
Wallet childWallet = walletDao.getMainWallet(schema);
|
||||
Wallet childWallet = walletDao.getMainWallet(schema, null);
|
||||
childWallet.setName(schema.substring(WALLET_SCHEMA_PREFIX.length()));
|
||||
childWallet.setMasterWallet(masterWallet);
|
||||
return childWallet;
|
||||
@@ -231,12 +235,14 @@ public class DbPersistence implements Persistence {
|
||||
if(addressNode.getId() == null) {
|
||||
WalletNode purposeNode = wallet.getNode(addressNode.getKeyPurpose());
|
||||
if(purposeNode.getId() == null) {
|
||||
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null);
|
||||
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null, null);
|
||||
purposeNode.setId(purposeNodeId);
|
||||
}
|
||||
|
||||
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId());
|
||||
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId(), addressNode.getAddressData());
|
||||
addressNode.setId(nodeId);
|
||||
} else if(addressNode.getAddress() != null) {
|
||||
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());
|
||||
@@ -285,12 +291,14 @@ public class DbPersistence implements Persistence {
|
||||
if(addressNode.getId() == null) {
|
||||
WalletNode purposeNode = wallet.getNode(addressNode.getKeyPurpose());
|
||||
if(purposeNode.getId() == null) {
|
||||
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null);
|
||||
long purposeNodeId = walletNodeDao.insertWalletNode(purposeNode.getDerivationPath(), purposeNode.getLabel(), wallet.getId(), null, null);
|
||||
purposeNode.setId(purposeNodeId);
|
||||
}
|
||||
|
||||
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId());
|
||||
long nodeId = walletNodeDao.insertWalletNode(addressNode.getDerivationPath(), addressNode.getLabel(), wallet.getId(), purposeNode.getId(), addressNode.getAddressData());
|
||||
addressNode.setId(nodeId);
|
||||
} else if(addressNode.getAddress() != null) {
|
||||
walletNodeDao.updateNodeAddressData(addressNode.getId(), addressNode.getAddressData());
|
||||
}
|
||||
|
||||
walletNodeDao.updateNodeLabel(addressNode.getId(), entry.getLabel());
|
||||
@@ -568,6 +576,11 @@ public class DbPersistence implements Persistence {
|
||||
com.google.common.io.Files.copy(walletFile, outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return dataSource.isClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
EventManager.get().unregister(this);
|
||||
@@ -661,7 +674,7 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
|
||||
private String getUrl(File walletFile, String password) {
|
||||
return "jdbc:h2:" + walletFile.getAbsolutePath().replace("." + getType().getExtension(), "") + ";INIT=SET TRACE_LEVEL_FILE=4;TRACE_LEVEL_FILE=4;DATABASE_TO_UPPER=false" + (password == null ? "" : ";CIPHER=AES");
|
||||
return "jdbc:h2:" + walletFile.getAbsolutePath().replace("." + getType().getExtension(), "") + ";INIT=SET TRACE_LEVEL_FILE=4;TRACE_LEVEL_FILE=4;DEFRAG_ALWAYS=true;MAX_COMPACT_TIME=5000;DATABASE_TO_UPPER=false" + (password == null ? "" : ";CIPHER=AES");
|
||||
}
|
||||
|
||||
private boolean persistsFor(Wallet wallet) {
|
||||
|
||||
@@ -69,9 +69,9 @@ public interface KeystoreDao {
|
||||
}
|
||||
|
||||
long id = insert(truncate(keystore.getLabel()), keystore.getSource().ordinal(), keystore.getWalletModel().ordinal(),
|
||||
keystore.hasMasterPrivateKey() ? null : keystore.getKeyDerivation().getMasterFingerprint(),
|
||||
keystore.hasMasterPrivateKey() || wallet.isBip47() ? null : keystore.getKeyDerivation().getMasterFingerprint(),
|
||||
keystore.getKeyDerivation().getDerivationPath(),
|
||||
keystore.hasMasterPrivateKey() ? null : keystore.getExtendedPublicKey().toString(),
|
||||
keystore.hasMasterPrivateKey() || wallet.isBip47() ? null : keystore.getExtendedPublicKey().toString(),
|
||||
keystore.getExternalPaymentCode() == null ? null : keystore.getExternalPaymentCode().toString(),
|
||||
keystore.getMasterPrivateExtendedKey() == null ? null : keystore.getMasterPrivateExtendedKey().getId(),
|
||||
keystore.getSeed() == null ? null : keystore.getSeed().getId(), wallet.getId(), i);
|
||||
|
||||
@@ -55,6 +55,9 @@ public interface WalletDao {
|
||||
@GetGeneratedKeys("id")
|
||||
long insert(String name, String label, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Integer watchLast, Date birthDate, long defaultPolicy);
|
||||
|
||||
@SqlUpdate("update wallet set name = :name where id = :id")
|
||||
void updateName(@Bind("id") long id, @Bind("name") String name);
|
||||
|
||||
@SqlUpdate("update wallet set label = :label where id = :id")
|
||||
void updateLabel(@Bind("id") long id, @Bind("label") String label);
|
||||
|
||||
@@ -70,12 +73,17 @@ public interface WalletDao {
|
||||
@SqlUpdate("set schema ?")
|
||||
int setSchema(String schema);
|
||||
|
||||
default Wallet getMainWallet(String schema) {
|
||||
default Wallet getMainWallet(String schema, String walletName) {
|
||||
try {
|
||||
setSchema(schema);
|
||||
Wallet mainWallet = loadMainWallet();
|
||||
if(mainWallet != null) {
|
||||
loadWallet(mainWallet);
|
||||
|
||||
if(walletName != null && !walletName.equals(mainWallet.getName())) {
|
||||
mainWallet.setName(walletName);
|
||||
updateName(mainWallet.getId(), walletName);
|
||||
}
|
||||
}
|
||||
|
||||
return mainWallet;
|
||||
@@ -100,7 +108,7 @@ public interface WalletDao {
|
||||
default void loadWallet(Wallet wallet) {
|
||||
wallet.getKeystores().addAll(createKeystoreDao().getForWalletId(wallet.getId()));
|
||||
|
||||
List<WalletNode> walletNodes = createWalletNodeDao().getForWalletId(wallet.getId());
|
||||
List<WalletNode> walletNodes = createWalletNodeDao().getForWalletId(wallet.getScriptType().ordinal(), wallet.getId());
|
||||
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList()));
|
||||
wallet.getPurposeNodes().forEach(walletNode -> walletNode.setWallet(wallet));
|
||||
|
||||
|
||||
@@ -16,18 +16,18 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public interface WalletNodeDao {
|
||||
@SqlQuery("select walletNode.id, walletNode.derivationPath, walletNode.label, walletNode.parent, " +
|
||||
@SqlQuery("select walletNode.id, walletNode.derivationPath, walletNode.label, walletNode.parent, walletNode.addressData, ?, " +
|
||||
"blockTransactionHashIndex.id, blockTransactionHashIndex.hash, blockTransactionHashIndex.height, blockTransactionHashIndex.date, blockTransactionHashIndex.fee, blockTransactionHashIndex.label, " +
|
||||
"blockTransactionHashIndex.index, blockTransactionHashIndex.outputValue, blockTransactionHashIndex.status, blockTransactionHashIndex.spentBy, blockTransactionHashIndex.node " +
|
||||
"from walletNode left join blockTransactionHashIndex on walletNode.id = blockTransactionHashIndex.node where walletNode.wallet = ? order by walletNode.parent asc nulls first, blockTransactionHashIndex.spentBy asc nulls first")
|
||||
@RegisterRowMapper(WalletNodeMapper.class)
|
||||
@RegisterRowMapper(BlockTransactionHashIndexMapper.class)
|
||||
@UseRowReducer(WalletNodeReducer.class)
|
||||
List<WalletNode> getForWalletId(Long id);
|
||||
List<WalletNode> getForWalletId(int scriptType, Long id);
|
||||
|
||||
@SqlUpdate("insert into walletNode (derivationPath, label, wallet, parent) values (?, ?, ?, ?)")
|
||||
@SqlUpdate("insert into walletNode (derivationPath, label, wallet, parent, addressData) values (?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
long insertWalletNode(String derivationPath, String label, long wallet, Long parent);
|
||||
long insertWalletNode(String derivationPath, String label, long wallet, Long parent, byte[] addressData);
|
||||
|
||||
@SqlUpdate("insert into blockTransactionHashIndex (hash, height, date, fee, label, index, outputValue, status, spentBy, node) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@GetGeneratedKeys("id")
|
||||
@@ -39,6 +39,9 @@ public interface WalletNodeDao {
|
||||
@SqlUpdate("update walletNode set label = :label where id = :id")
|
||||
void updateNodeLabel(@Bind("id") long id, @Bind("label") String label);
|
||||
|
||||
@SqlUpdate("update walletNode set addressData = :addressData where id = :id and addressData is null")
|
||||
void updateNodeAddressData(@Bind("id") long id, @Bind("addressData") byte[] addressData);
|
||||
|
||||
@SqlUpdate("update blockTransactionHashIndex set label = :label where id = :id")
|
||||
void updateTxoLabel(@Bind("id") long id, @Bind("label") String label);
|
||||
|
||||
@@ -59,12 +62,12 @@ public interface WalletNodeDao {
|
||||
|
||||
default void addWalletNodes(Wallet wallet) {
|
||||
for(WalletNode purposeNode : wallet.getPurposeNodes()) {
|
||||
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), truncate(purposeNode.getLabel()), wallet.getId(), null);
|
||||
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), truncate(purposeNode.getLabel()), wallet.getId(), null, null);
|
||||
purposeNode.setId(purposeNodeId);
|
||||
addTransactionOutputs(purposeNode);
|
||||
List<WalletNode> childNodes = new ArrayList<>(purposeNode.getChildren());
|
||||
for(WalletNode addressNode : childNodes) {
|
||||
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), truncate(addressNode.getLabel()), wallet.getId(), purposeNodeId);
|
||||
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), truncate(addressNode.getLabel()), wallet.getId(), purposeNodeId, addressNode.getAddressData());
|
||||
addressNode.setId(addressNodeId);
|
||||
addTransactionOutputs(addressNode);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
@@ -13,7 +14,11 @@ public class WalletNodeMapper implements RowMapper<WalletNode> {
|
||||
WalletNode walletNode = new WalletNode(rs.getString("walletNode.derivationPath"));
|
||||
walletNode.setId(rs.getLong("walletNode.id"));
|
||||
walletNode.setLabel(rs.getString("walletNode.label"));
|
||||
|
||||
byte[] addressData = rs.getBytes("walletNode.addressData");
|
||||
if(addressData != null) {
|
||||
ScriptType scriptType = ScriptType.values()[rs.getInt(6)];
|
||||
walletNode.setAddress(scriptType.getAddress(addressData));
|
||||
}
|
||||
return walletNode;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-2
@@ -19,6 +19,7 @@ import javafx.scene.layout.StackPane;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class KeystoreImportController implements Initializable {
|
||||
@@ -51,7 +52,7 @@ public class KeystoreImportController implements Initializable {
|
||||
}
|
||||
|
||||
KeystoreSource importType = (KeystoreSource) selectedToggle.getUserData();
|
||||
String fxmlName = importType.toString().toLowerCase();
|
||||
String fxmlName = importType.toString().toLowerCase(Locale.ROOT);
|
||||
if(importType == KeystoreSource.SW_SEED || importType == KeystoreSource.SW_WATCH) {
|
||||
fxmlName = "sw";
|
||||
}
|
||||
@@ -91,7 +92,12 @@ public class KeystoreImportController implements Initializable {
|
||||
importPane.getChildren().removeAll(importPane.getChildren());
|
||||
|
||||
try {
|
||||
FXMLLoader importLoader = new FXMLLoader(AppServices.class.getResource("keystoreimport/" + fxmlName + ".fxml"));
|
||||
URL url = AppServices.class.getResource("keystoreimport/" + fxmlName + ".fxml");
|
||||
if(url == null) {
|
||||
throw new IllegalStateException("Cannot find keystoreimport/" + fxmlName + ".fxml");
|
||||
}
|
||||
|
||||
FXMLLoader importLoader = new FXMLLoader(url);
|
||||
Node importTypeNode = importLoader.load();
|
||||
KeystoreImportDetailController controller = importLoader.getController();
|
||||
controller.setMasterController(this);
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
public class Auth47 {
|
||||
private static final Logger log = LoggerFactory.getLogger(Auth47.class);
|
||||
|
||||
public static final String SCHEME = "auth47";
|
||||
public static final String VERSION = "1.0";
|
||||
public static final String HTTPS_PROTOCOL = "https://";
|
||||
public static final String SRBN_PROTOCOL = "srbn://";
|
||||
|
||||
private final String nonce;
|
||||
private final URL callback;
|
||||
private final String expiry;
|
||||
private boolean srbn;
|
||||
private String srbnName;
|
||||
private String resource;
|
||||
|
||||
public Auth47(URI uri) throws MalformedURLException {
|
||||
this.nonce = uri.getHost();
|
||||
|
||||
Map<String, String> parameterMap = new LinkedHashMap<>();
|
||||
String query = uri.getRawQuery();
|
||||
if(query == null) {
|
||||
throw new IllegalArgumentException("No callback parameter provided.");
|
||||
}
|
||||
|
||||
if(query.startsWith("?")) {
|
||||
query = query.substring(1);
|
||||
}
|
||||
|
||||
String[] pairs = query.split("&");
|
||||
for(String pair : pairs) {
|
||||
int idx = pair.indexOf("=");
|
||||
if(idx < 0) {
|
||||
continue;
|
||||
}
|
||||
parameterMap.put(pair.substring(0, idx), pair.substring(idx + 1));
|
||||
}
|
||||
|
||||
String strCallback = parameterMap.get("c");
|
||||
if(strCallback == null) {
|
||||
throw new IllegalArgumentException("No callback parameter provided.");
|
||||
}
|
||||
if(strCallback.startsWith(SRBN_PROTOCOL)) {
|
||||
String srbnCallback = HTTPS_PROTOCOL + strCallback.substring(SRBN_PROTOCOL.length());
|
||||
URL srbnUrl = new URL(srbnCallback);
|
||||
this.srbn = true;
|
||||
this.srbnName = srbnUrl.getUserInfo();
|
||||
this.callback = new URL(HTTPS_PROTOCOL + srbnUrl.getHost());
|
||||
} else {
|
||||
this.callback = new URL(strCallback);
|
||||
}
|
||||
|
||||
this.expiry = parameterMap.get("e");
|
||||
this.resource = parameterMap.get("r");
|
||||
if(resource == null) {
|
||||
if(srbn) {
|
||||
this.resource = "srbn";
|
||||
} else if(strCallback.startsWith("http")) {
|
||||
this.resource = strCallback;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid callback parameter (not http/s or srbn): " + strCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void sendResponse(Wallet wallet) throws IOException, Auth47Exception {
|
||||
if(!wallet.hasPaymentCode()) {
|
||||
throw new Auth47Exception("A software wallet is required to authenticate.");
|
||||
}
|
||||
|
||||
if(srbn) {
|
||||
sendSorobanResponse(wallet);
|
||||
} else {
|
||||
sendHttpResponse(wallet);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendHttpResponse(Wallet wallet) throws IOException, Auth47Exception {
|
||||
String json = getJsonResponse(wallet);
|
||||
|
||||
send(json);
|
||||
}
|
||||
|
||||
public void sendSorobanResponse(Wallet wallet) throws IOException, Auth47Exception {
|
||||
String json = getJsonResponse(wallet);
|
||||
|
||||
SorobanResponse sorobanResponse = new SorobanResponse(srbnName, json);
|
||||
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
|
||||
String jsonRpc = gson.toJson(sorobanResponse);
|
||||
|
||||
send(jsonRpc);
|
||||
}
|
||||
|
||||
private String getJsonResponse(Wallet wallet) {
|
||||
String challenge = getChallenge();
|
||||
String signature = sign(wallet, challenge);
|
||||
|
||||
Response response = new Response(VERSION, challenge, signature, wallet.getPaymentCode().toString(), null);
|
||||
Gson gson = new GsonBuilder().disableHtmlEscaping().create();
|
||||
return gson.toJson(response);
|
||||
}
|
||||
|
||||
private void send(String json) throws IOException, Auth47Exception {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("Sending " + json + " to " + callback);
|
||||
}
|
||||
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
if(proxy == null && callback.getHost().toLowerCase(Locale.ROOT).endsWith(TorService.TOR_ADDRESS_SUFFIX)) {
|
||||
throw new Auth47Exception("A Tor proxy must be configured to authenticate this resource.");
|
||||
}
|
||||
|
||||
HttpURLConnection connection = proxy == null ? (HttpURLConnection) callback.openConnection() : (HttpURLConnection) callback.openConnection(proxy);
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setDoOutput(true);
|
||||
|
||||
try(OutputStream os = connection.getOutputStream()) {
|
||||
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
|
||||
os.write(jsonBytes);
|
||||
}
|
||||
|
||||
int statusCode = connection.getResponseCode();
|
||||
if(statusCode == 404) {
|
||||
throw new Auth47Exception("Could not authenticate. Callback URL of " + callback + " returned a 404 response.");
|
||||
}
|
||||
|
||||
StringBuilder res = new StringBuilder();
|
||||
try(BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String responseLine;
|
||||
while((responseLine = br.readLine()) != null) {
|
||||
res.append(responseLine.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("Received " + statusCode + " " + res);
|
||||
}
|
||||
|
||||
if(statusCode < 200 || statusCode >= 300) {
|
||||
throw new Auth47Exception("Could not authenticate. Server returned " + res);
|
||||
}
|
||||
}
|
||||
|
||||
private String sign(Wallet wallet, String challenge) {
|
||||
try {
|
||||
Wallet notificationWallet = wallet.getNotificationWallet();
|
||||
WalletNode notificationNode = notificationWallet.getNode(KeyPurpose.NOTIFICATION);
|
||||
ExtendedKey extendedPrivateKey = notificationWallet.getKeystores().get(0).getBip47ExtendedPrivateKey();
|
||||
List<ChildNumber> derivation = new ArrayList<>();
|
||||
derivation.add(extendedPrivateKey.getKeyChildNumber());
|
||||
derivation.addAll(notificationNode.getDerivation());
|
||||
ECKey privKey = extendedPrivateKey.getKey(derivation);
|
||||
return privKey.signMessage(challenge, ScriptType.P2PKH);
|
||||
} catch(Exception e) {
|
||||
log.error("Error signing auth47 challenge", e);
|
||||
throw new IllegalStateException("Error signing auth47 challenge: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getChallenge() {
|
||||
return SCHEME + "://" + nonce + "?r=" + resource + (expiry == null ? "" : "&e=" + expiry);
|
||||
}
|
||||
|
||||
public URL getCallback() {
|
||||
return callback;
|
||||
}
|
||||
|
||||
private static class Response {
|
||||
public Response(String version, String challenge, String signature, String paymentCode, String address) {
|
||||
this.auth47_response = version;
|
||||
this.challenge = challenge;
|
||||
this.signature = signature;
|
||||
this.nym = paymentCode;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public String auth47_response;
|
||||
public String challenge;
|
||||
public String signature;
|
||||
public String nym;
|
||||
public String address;
|
||||
}
|
||||
|
||||
private static class SorobanResponse {
|
||||
public SorobanResponse(String name, String response) {
|
||||
params.add(new SorobanParam(name, response));
|
||||
}
|
||||
|
||||
public String jsonrpc = "2.0";
|
||||
public int id = 0;
|
||||
public String method = "directory.Add";
|
||||
public List<SorobanParam> params = new ArrayList<>();
|
||||
}
|
||||
|
||||
private static class SorobanParam {
|
||||
public SorobanParam(String name, String entry) {
|
||||
Name = name;
|
||||
Entry = entry;
|
||||
}
|
||||
|
||||
public String Name;
|
||||
public String Entry;
|
||||
public String Mode = "short";
|
||||
}
|
||||
|
||||
public static final class Auth47Exception extends Exception {
|
||||
public Auth47Exception(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public Auth47Exception(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public Auth47Exception(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public Auth47Exception(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
@@ -31,6 +32,8 @@ public class Bwt {
|
||||
private static final Logger log = LoggerFactory.getLogger(Bwt.class);
|
||||
|
||||
public static final String DEFAULT_CORE_WALLET = "sparrow";
|
||||
public static final String ELECTRUM_HOST = "127.0.0.1";
|
||||
public static final String ELECTRUM_PORT = "0";
|
||||
private static final int IMPORT_BATCH_SIZE = 350;
|
||||
private static boolean initialized;
|
||||
private Long shutdownPtr;
|
||||
@@ -41,7 +44,10 @@ public class Bwt {
|
||||
if(!initialized) {
|
||||
try {
|
||||
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
|
||||
if(platform == org.controlsfx.tools.Platform.OSX) {
|
||||
String osArch = System.getProperty("os.arch");
|
||||
if(platform == org.controlsfx.tools.Platform.OSX && osArch.equals("aarch64")) {
|
||||
NativeUtils.loadLibraryFromJar("/native/osx/aarch64/libbwt_jni.dylib");
|
||||
} else if(platform == org.controlsfx.tools.Platform.OSX) {
|
||||
NativeUtils.loadLibraryFromJar("/native/osx/x64/libbwt_jni.dylib");
|
||||
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
NativeUtils.loadLibraryFromJar("/native/windows/x64/bwt_jni.dll");
|
||||
@@ -56,18 +62,35 @@ public class Bwt {
|
||||
}
|
||||
|
||||
private void start(CallbackNotifier callback) {
|
||||
start(Collections.emptyList(), null, null, null, callback);
|
||||
start(Collections.emptyList(), Collections.emptyList(), null, null, null, callback);
|
||||
}
|
||||
|
||||
private void start(Collection<Wallet> wallets, CallbackNotifier callback) {
|
||||
List<Wallet> validWallets = wallets.stream().filter(Wallet::isValid).collect(Collectors.toList());
|
||||
|
||||
Set<String> outputDescriptors = new LinkedHashSet<>();
|
||||
Set<String> addresses = new LinkedHashSet<>();
|
||||
for(Wallet wallet : validWallets) {
|
||||
OutputDescriptor receiveOutputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.RECEIVE);
|
||||
outputDescriptors.add(receiveOutputDescriptor.toString(false, false));
|
||||
OutputDescriptor changeOutputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.CHANGE);
|
||||
outputDescriptors.add(changeOutputDescriptor.toString(false, false));
|
||||
|
||||
if(wallet.isMasterWallet() && wallet.hasPaymentCode()) {
|
||||
Wallet notificationWallet = wallet.getNotificationWallet();
|
||||
WalletNode notificationNode = notificationWallet.getNode(KeyPurpose.NOTIFICATION);
|
||||
addresses.add(notificationNode.getAddress().toString());
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isNested()) {
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
for(WalletNode addressNode : childWallet.getNode(keyPurpose).getChildren()) {
|
||||
addresses.add(addressNode.getAddress().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int rescanSince = validWallets.stream().filter(wallet -> wallet.getBirthDate() != null).mapToInt(wallet -> (int)(wallet.getBirthDate().getTime() / 1000)).min().orElse(-1);
|
||||
@@ -81,7 +104,7 @@ public class Bwt {
|
||||
}
|
||||
}
|
||||
|
||||
start(outputDescriptors, rescanSince, forceRescan, gapLimit, callback);
|
||||
start(outputDescriptors, addresses, rescanSince, forceRescan, gapLimit, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +116,7 @@ public class Bwt {
|
||||
* @param gapLimit desired gap limit beyond last used address
|
||||
* @param callback object receiving notifications
|
||||
*/
|
||||
private void start(Collection<String> outputDescriptors, Integer rescanSince, Boolean forceRescan, Integer gapLimit, CallbackNotifier callback) {
|
||||
private void start(Collection<String> outputDescriptors, Collection<String> addresses, Integer rescanSince, Boolean forceRescan, Integer gapLimit, CallbackNotifier callback) {
|
||||
BwtConfig bwtConfig = new BwtConfig();
|
||||
bwtConfig.network = Network.get() == Network.MAINNET ? "bitcoin" : Network.get().getName();
|
||||
|
||||
@@ -102,6 +125,10 @@ public class Bwt {
|
||||
bwtConfig.rescanSince = (rescanSince == null || rescanSince < 0 ? "now" : rescanSince);
|
||||
bwtConfig.forceRescan = forceRescan;
|
||||
bwtConfig.gapLimit = gapLimit;
|
||||
|
||||
if(!addresses.isEmpty()) {
|
||||
bwtConfig.addresses = addresses;
|
||||
}
|
||||
} else {
|
||||
bwtConfig.requireAddresses = false;
|
||||
bwtConfig.bitcoindTimeout = 30;
|
||||
@@ -112,7 +139,7 @@ public class Bwt {
|
||||
bwtConfig.setupLogger = false;
|
||||
}
|
||||
|
||||
bwtConfig.electrumAddr = "127.0.0.1:0";
|
||||
bwtConfig.electrumAddr = ELECTRUM_HOST + ":" + ELECTRUM_PORT;
|
||||
bwtConfig.electrumSkipMerkle = true;
|
||||
|
||||
Config config = Config.get();
|
||||
@@ -184,8 +211,8 @@ public class Bwt {
|
||||
return terminating;
|
||||
}
|
||||
|
||||
public ConnectionService getConnectionService(Collection<Wallet> wallets) {
|
||||
return wallets != null ? new ConnectionService(wallets) : new ConnectionService();
|
||||
public ConnectionService getConnectionService(boolean useWallets) {
|
||||
return new ConnectionService(useWallets);
|
||||
}
|
||||
|
||||
public DisconnectionService getDisconnectionService() {
|
||||
@@ -223,6 +250,9 @@ public class Bwt {
|
||||
@SerializedName("descriptors")
|
||||
public Collection<String> descriptors;
|
||||
|
||||
@SerializedName("addresses")
|
||||
public Collection<String> addresses;
|
||||
|
||||
@SerializedName("xpubs")
|
||||
public String xpubs;
|
||||
|
||||
@@ -258,14 +288,10 @@ public class Bwt {
|
||||
}
|
||||
|
||||
public final class ConnectionService extends Service<Void> {
|
||||
private final Collection<Wallet> wallets;
|
||||
private final boolean useWallets;
|
||||
|
||||
public ConnectionService() {
|
||||
this.wallets = null;
|
||||
}
|
||||
|
||||
public ConnectionService(Collection<Wallet> wallets) {
|
||||
this.wallets = wallets;
|
||||
public ConnectionService(boolean useWallets) {
|
||||
this.useWallets = useWallets;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -329,10 +355,10 @@ public class Bwt {
|
||||
}
|
||||
};
|
||||
|
||||
if(wallets == null) {
|
||||
if(!useWallets) {
|
||||
Bwt.this.start(notifier);
|
||||
} else {
|
||||
Bwt.this.start(wallets, notifier);
|
||||
Bwt.this.start(AppServices.get().getOpenWallets().keySet(), notifier);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.github.arteam.simplejsonrpc.client.Transport;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
public interface CloseableTransport extends Transport, Closeable {
|
||||
void connect() throws ServerException;
|
||||
boolean isConnected();
|
||||
boolean isClosed();
|
||||
}
|
||||
@@ -47,7 +47,7 @@ public class ElectrumServer {
|
||||
|
||||
public static final BlockTransaction UNFETCHABLE_BLOCK_TRANSACTION = new BlockTransaction(Sha256Hash.ZERO_HASH, 0, null, null, null);
|
||||
|
||||
private static Transport transport;
|
||||
private static CloseableTransport transport;
|
||||
|
||||
private static final Map<String, List<String>> subscribedScriptHashes = Collections.synchronizedMap(new HashMap<>());
|
||||
|
||||
@@ -63,7 +63,7 @@ public class ElectrumServer {
|
||||
|
||||
private static final Pattern RPC_WALLET_LOADING_PATTERN = Pattern.compile(".*\"(Wallet loading failed:[^\"]*)\".*");
|
||||
|
||||
private static synchronized Transport getTransport() throws ServerException {
|
||||
private static synchronized CloseableTransport getTransport() throws ServerException {
|
||||
if(transport == null) {
|
||||
try {
|
||||
String electrumServer = null;
|
||||
@@ -78,6 +78,9 @@ public class ElectrumServer {
|
||||
throw new ServerConfigException("Could not connect to Bitcoin Core RPC");
|
||||
}
|
||||
electrumServer = bwtElectrumServer;
|
||||
if(previousServerAddress != null && previousServerAddress.contains(Bwt.ELECTRUM_HOST)) {
|
||||
previousServerAddress = bwtElectrumServer;
|
||||
}
|
||||
} else if(Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
|
||||
electrumServer = Config.get().getElectrumServer();
|
||||
electrumServerCert = Config.get().getElectrumServerCert();
|
||||
@@ -130,8 +133,8 @@ public class ElectrumServer {
|
||||
}
|
||||
|
||||
public void connect() throws ServerException {
|
||||
TcpTransport tcpTransport = (TcpTransport)getTransport();
|
||||
tcpTransport.connect();
|
||||
CloseableTransport closeableTransport = getTransport();
|
||||
closeableTransport.connect();
|
||||
}
|
||||
|
||||
public void ping() throws ServerException {
|
||||
@@ -160,12 +163,15 @@ public class ElectrumServer {
|
||||
}
|
||||
|
||||
public static synchronized void closeActiveConnection() throws ServerException {
|
||||
if(transport != null) {
|
||||
closeConnection(transport);
|
||||
transport = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void closeConnection(Closeable closeableTransport) throws ServerException {
|
||||
try {
|
||||
if(transport != null) {
|
||||
Closeable closeableTransport = (Closeable)transport;
|
||||
closeableTransport.close();
|
||||
transport = null;
|
||||
}
|
||||
closeableTransport.close();
|
||||
} catch (IOException e) {
|
||||
throw new ServerException(e);
|
||||
}
|
||||
@@ -429,6 +435,7 @@ public class ElectrumServer {
|
||||
try {
|
||||
Set<String> scriptHashes = new HashSet<>();
|
||||
Map<String, String> pathScriptHashes = new LinkedHashMap<>();
|
||||
Map<String, WalletNode> pathNodes = new HashMap<>();
|
||||
for(WalletNode node : nodes) {
|
||||
if(node == null) {
|
||||
log.error("Null node for wallet " + wallet.getFullName() + " subscribing nodes " + nodes + " startIndex " + startIndex, new Throwable());
|
||||
@@ -445,6 +452,7 @@ public class ElectrumServer {
|
||||
} else if(!subscribedScriptHashes.containsKey(scriptHash) && scriptHashes.add(scriptHash)) {
|
||||
//Unique script hash we are not yet subscribed to
|
||||
pathScriptHashes.put(node.getDerivationPath(), scriptHash);
|
||||
pathNodes.put(node.getDerivationPath(), node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,9 +468,8 @@ public class ElectrumServer {
|
||||
for(String path : result.keySet()) {
|
||||
String status = result.get(path);
|
||||
|
||||
Optional<WalletNode> optionalNode = nodes.stream().filter(n -> n.getDerivationPath().equals(path)).findFirst();
|
||||
if(optionalNode.isPresent()) {
|
||||
WalletNode node = optionalNode.get();
|
||||
WalletNode node = pathNodes.computeIfAbsent(path, p -> nodes.stream().filter(n -> n.getDerivationPath().equals(p)).findFirst().orElse(null));
|
||||
if(node != null) {
|
||||
String scriptHash = getScriptHash(wallet, node);
|
||||
|
||||
//Check if there is history for this script hash, and if the history has changed since last fetched
|
||||
@@ -737,6 +744,7 @@ public class ElectrumServer {
|
||||
if(!transactionOutputs.equals(node.getTransactionOutputs())) {
|
||||
node.updateTransactionOutputs(wallet, transactionOutputs);
|
||||
copyPostmixLabels(wallet, transactionOutputs);
|
||||
copyBadbankLabels(wallet, transactionOutputs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -754,6 +762,21 @@ public class ElectrumServer {
|
||||
}
|
||||
}
|
||||
|
||||
public void copyBadbankLabels(Wallet wallet, Set<BlockTransactionHashIndex> newTransactionOutputs) {
|
||||
if(wallet.getStandardAccountType() == StandardAccount.WHIRLPOOL_BADBANK && wallet.getMasterWallet() != null) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> masterWalletTxos = wallet.getMasterWallet().getWalletTxos();
|
||||
for(BlockTransactionHashIndex newRef : newTransactionOutputs) {
|
||||
BlockTransactionHashIndex prevRef = masterWalletTxos.keySet().stream()
|
||||
.filter(txo -> txo.isSpent() && txo.getSpentBy().getHash().equals(newRef.getHash()) && txo.getLabel() != null).findFirst().orElse(null);
|
||||
if(prevRef != null) {
|
||||
if(newRef.getLabel() == null && prevRef.getLabel() != null) {
|
||||
newRef.setLabel("From " + prevRef.getLabel());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Map<Sha256Hash, BlockTransaction> getReferencedTransactions(Set<Sha256Hash> references, String scriptHash) throws ServerException {
|
||||
Set<String> txids = new LinkedHashSet<>(references.size());
|
||||
for(Sha256Hash reference : references) {
|
||||
@@ -967,7 +990,7 @@ public class ElectrumServer {
|
||||
|
||||
public static boolean supportsBatching(List<String> serverVersion) {
|
||||
if(serverVersion.size() > 0) {
|
||||
String server = serverVersion.get(0).toLowerCase();
|
||||
String server = serverVersion.get(0).toLowerCase(Locale.ROOT);
|
||||
if(server.contains("electrumx")) {
|
||||
return true;
|
||||
}
|
||||
@@ -1062,7 +1085,7 @@ public class ElectrumServer {
|
||||
Bwt.initialize();
|
||||
|
||||
if(!bwt.isRunning()) {
|
||||
Bwt.ConnectionService bwtConnectionService = bwt.getConnectionService(subscribe ? AppServices.get().getOpenWallets().keySet() : null);
|
||||
Bwt.ConnectionService bwtConnectionService = bwt.getConnectionService(subscribe);
|
||||
bwtStartException = null;
|
||||
bwtConnectionService.setOnFailed(workerStateEvent -> {
|
||||
log.error("Failed to start BWT", workerStateEvent.getSource().getException());
|
||||
@@ -1176,6 +1199,10 @@ public class ElectrumServer {
|
||||
return isRunning() && Config.get().getServerType() == ServerType.BITCOIN_CORE && bwt.isRunning() && !bwt.isReady();
|
||||
}
|
||||
|
||||
public boolean isConnectionRunning() {
|
||||
return isRunning() && (Config.get().getServerType() != ServerType.BITCOIN_CORE || bwt.isRunning());
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return isRunning() && (Config.get().getServerType() != ServerType.BITCOIN_CORE || (bwt.isRunning() && bwt.isReady()));
|
||||
}
|
||||
@@ -1580,17 +1607,26 @@ public class ElectrumServer {
|
||||
if(nodeTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
|
||||
Wallet masterWalletCopy = wallet.copy();
|
||||
List<StandardAccount> searchAccounts = getStandardAccounts(wallet);
|
||||
Set<StandardAccount> foundAccounts = new LinkedHashSet<>();
|
||||
for(int j = 0; j < searchAccounts.size(); j++) {
|
||||
StandardAccount standardAccount = searchAccounts.get(j);
|
||||
Wallet childWallet = masterWalletCopy.addChildWallet(standardAccount);
|
||||
Map<WalletNode, Set<BlockTransactionHash>> childTransactionMap = new TreeMap<>();
|
||||
electrumServer.getReferences(childWallet, childWallet.getNode(KeyPurpose.RECEIVE).getChildren(), childTransactionMap, 0);
|
||||
if(childTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
|
||||
wallet.addChildWallet(standardAccount);
|
||||
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
|
||||
foundAccounts.addAll(StandardAccount.WHIRLPOOL_ACCOUNTS);
|
||||
} else {
|
||||
foundAccounts.add(standardAccount);
|
||||
}
|
||||
}
|
||||
updateProgress(i + j, wallets.size() + StandardAccount.values().length);
|
||||
}
|
||||
|
||||
for(StandardAccount standardAccount : foundAccounts) {
|
||||
wallet.addChildWallet(standardAccount);
|
||||
}
|
||||
|
||||
return Optional.of(wallet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ public enum ExchangeSource {
|
||||
COINBASE("Coinbase") {
|
||||
@Override
|
||||
public List<Currency> getSupportedCurrencies() {
|
||||
return getRates().data.rates.keySet().stream().filter(code -> isValidISO4217Code(code.toUpperCase()))
|
||||
.map(code -> Currency.getInstance(code.toUpperCase())).collect(Collectors.toList());
|
||||
return getRates().data.rates.keySet().stream().filter(code -> isValidISO4217Code(code.toUpperCase(Locale.ROOT)))
|
||||
.map(code -> Currency.getInstance(code.toUpperCase(Locale.ROOT))).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -68,8 +68,8 @@ public enum ExchangeSource {
|
||||
COINGECKO("Coingecko") {
|
||||
@Override
|
||||
public List<Currency> getSupportedCurrencies() {
|
||||
return getRates().rates.entrySet().stream().filter(rate -> "fiat".equals(rate.getValue().type) && isValidISO4217Code(rate.getKey().toUpperCase()))
|
||||
.map(rate -> Currency.getInstance(rate.getKey().toUpperCase())).collect(Collectors.toList());
|
||||
return getRates().rates.entrySet().stream().filter(rate -> "fiat".equals(rate.getValue().type) && isValidISO4217Code(rate.getKey().toUpperCase(Locale.ROOT)))
|
||||
.map(rate -> Currency.getInstance(rate.getKey().toUpperCase(Locale.ROOT))).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
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.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.Bech32;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class LnurlAuth {
|
||||
private static final Logger log = LoggerFactory.getLogger(LnurlAuth.class);
|
||||
|
||||
public static final ChildNumber LNURL_PURPOSE = new ChildNumber(138, true);
|
||||
|
||||
private final URL url;
|
||||
private final byte[] k1;
|
||||
private final String action;
|
||||
|
||||
public LnurlAuth(URI uri) throws MalformedURLException {
|
||||
String lnurl = uri.getSchemeSpecificPart();
|
||||
Bech32.Bech32Data bech32 = Bech32.decode(lnurl, 2000);
|
||||
byte[] urlBytes = Bech32.convertBits(bech32.data, 0, bech32.data.length, 5, 8, false);
|
||||
String strUrl = new String(urlBytes, StandardCharsets.UTF_8);
|
||||
this.url = new URL(strUrl);
|
||||
|
||||
Map<String, String> parameterMap = new LinkedHashMap<>();
|
||||
String query = url.getQuery();
|
||||
if(query == null) {
|
||||
throw new IllegalArgumentException("No k1 parameter provided.");
|
||||
}
|
||||
|
||||
if(query.startsWith("?")) {
|
||||
query = query.substring(1);
|
||||
}
|
||||
|
||||
String[] pairs = query.split("&");
|
||||
for(String pair : pairs) {
|
||||
int idx = pair.indexOf("=");
|
||||
if(idx < 0) {
|
||||
continue;
|
||||
}
|
||||
parameterMap.put(pair.substring(0, idx), pair.substring(idx + 1));
|
||||
}
|
||||
|
||||
if(parameterMap.get("tag") == null || !parameterMap.get("tag").toLowerCase(Locale.ROOT).equals("login")) {
|
||||
throw new IllegalArgumentException("Parameter tag was not set to login.");
|
||||
}
|
||||
|
||||
if(parameterMap.get("k1") == null || parameterMap.get("k1").length() != 64) {
|
||||
throw new IllegalArgumentException("Parameter k1 was absent or of incorrect length.");
|
||||
}
|
||||
|
||||
try {
|
||||
this.k1 = Utils.hexToBytes(parameterMap.get("k1"));
|
||||
} catch(Exception e) {
|
||||
throw new IllegalArgumentException("Parameter k1 was not a valid hexadecimal value.");
|
||||
}
|
||||
|
||||
this.action = parameterMap.get("action") == null ? "login" : parameterMap.get("action").toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public String getDomain() {
|
||||
return url.getHost();
|
||||
}
|
||||
|
||||
public String getLoginMessage() {
|
||||
String domain = getDomain();
|
||||
switch(action) {
|
||||
case "register":
|
||||
return "register an account on " + domain;
|
||||
case "link":
|
||||
return "link your existing account on " + domain;
|
||||
case "auth":
|
||||
return "authorise " + domain;
|
||||
case "login":
|
||||
default:
|
||||
return "login to " + domain;
|
||||
}
|
||||
}
|
||||
|
||||
public void sendResponse(Wallet wallet) throws LnurlAuthException, IOException {
|
||||
URL callback = getReturnURL(wallet);
|
||||
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
if(proxy == null && callback.getHost().toLowerCase(Locale.ROOT).endsWith(TorService.TOR_ADDRESS_SUFFIX)) {
|
||||
throw new LnurlAuthException("A Tor proxy must be configured to authenticate this resource.");
|
||||
}
|
||||
|
||||
HttpURLConnection connection = proxy == null ? (HttpURLConnection) callback.openConnection() : (HttpURLConnection) callback.openConnection(proxy);
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
|
||||
StringBuilder res = new StringBuilder();
|
||||
try(BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String responseLine;
|
||||
while((responseLine = br.readLine()) != null) {
|
||||
res.append(responseLine.trim());
|
||||
}
|
||||
}
|
||||
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("Received from " + callback + ": " + res);
|
||||
}
|
||||
|
||||
JsonObject result = JsonParser.parseString(res.toString()).getAsJsonObject();
|
||||
String status = result.get("status").getAsString();
|
||||
if("OK".equals(status)) {
|
||||
return;
|
||||
} else if("ERROR".equals(status)) {
|
||||
String reason = result.get("reason").getAsString();
|
||||
throw new LnurlAuthException("Service returned error: " + reason);
|
||||
} else {
|
||||
throw new LnurlAuthException("Service returned unknown response: " + res);
|
||||
}
|
||||
}
|
||||
|
||||
private URL getReturnURL(Wallet wallet) {
|
||||
try {
|
||||
ECKey linkingKey = deriveLinkingKey(wallet);
|
||||
byte[] signature = getSignature(linkingKey);
|
||||
return new URL(url.toString() + "&sig=" + Utils.bytesToHex(signature) + "&key=" + Utils.bytesToHex(linkingKey.getPubKey()));
|
||||
} catch(MalformedURLException e) {
|
||||
throw new IllegalStateException("Malformed return URL", e);
|
||||
}
|
||||
}
|
||||
|
||||
private ECKey deriveLinkingKey(Wallet wallet) {
|
||||
if(wallet.getPolicyType() != PolicyType.SINGLE) {
|
||||
throw new IllegalArgumentException("Only singlesig wallets can authenticate.");
|
||||
}
|
||||
|
||||
if(wallet.isEncrypted()) {
|
||||
throw new IllegalArgumentException("Wallet must be decrypted.");
|
||||
}
|
||||
|
||||
try {
|
||||
ExtendedKey masterPrivateKey = wallet.getKeystores().get(0).getExtendedMasterPrivateKey();
|
||||
ECKey hashingKey = masterPrivateKey.getKey(List.of(LNURL_PURPOSE, ChildNumber.ZERO));
|
||||
byte[] hash = getHmacSha256Hash(hashingKey.getPrivKeyBytes(), getDomain());
|
||||
List<ChildNumber> pathIndexes = IntStream.range(0, 4).mapToLong(i -> ByteBuffer.wrap(hash, i * 4, 4).getInt() & 0xFFFFFFFFL)
|
||||
.mapToObj(i -> new ChildNumber((int)i)).collect(Collectors.toList());
|
||||
|
||||
List<ChildNumber> derivationPath = new ArrayList<>();
|
||||
derivationPath.add(LNURL_PURPOSE);
|
||||
derivationPath.addAll(pathIndexes);
|
||||
return masterPrivateKey.getKey(derivationPath);
|
||||
} catch(Exception e) {
|
||||
throw new IllegalStateException("Could not determine linking key", e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getSignature(ECKey linkingKey) {
|
||||
ECDSASignature ecdsaSignature = linkingKey.signEcdsa(Sha256Hash.wrap(k1));
|
||||
return ecdsaSignature.encodeToDER();
|
||||
}
|
||||
|
||||
private static byte[] getHmacSha256Hash(byte[] key, String data) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec secret_key = new SecretKeySpec(key, "HmacSHA256");
|
||||
sha256_HMAC.init(secret_key);
|
||||
|
||||
return sha256_HMAC.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public static final class LnurlAuthException extends Exception {
|
||||
public LnurlAuthException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public LnurlAuthException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public LnurlAuthException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public LnurlAuthException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,12 @@ import java.security.KeyManagementException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum Protocol {
|
||||
TCP {
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server) {
|
||||
public CloseableTransport getTransport(HostAndPort server) {
|
||||
if(isOnionAddress(server)) {
|
||||
return new TorTcpTransport(server);
|
||||
}
|
||||
@@ -22,24 +23,24 @@ public enum Protocol {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, File serverCert) {
|
||||
public CloseableTransport getTransport(HostAndPort server, File serverCert) {
|
||||
return getTransport(server);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, HostAndPort proxy) {
|
||||
public CloseableTransport getTransport(HostAndPort server, HostAndPort proxy) {
|
||||
//Avoid using a TorSocket if a proxy is specified, even if a .onion address
|
||||
return new TcpTransport(server, proxy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) {
|
||||
public CloseableTransport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) {
|
||||
return getTransport(server, proxy);
|
||||
}
|
||||
},
|
||||
SSL {
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
public CloseableTransport getTransport(HostAndPort server) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
if(isOnionAddress(server)) {
|
||||
return new TorTcpOverTlsTransport(server);
|
||||
}
|
||||
@@ -48,7 +49,7 @@ public enum Protocol {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
public CloseableTransport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
if(isOnionAddress(server)) {
|
||||
return new TorTcpOverTlsTransport(server, serverCert);
|
||||
}
|
||||
@@ -57,51 +58,51 @@ public enum Protocol {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
public CloseableTransport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
return new ProxyTcpOverTlsTransport(server, proxy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
public CloseableTransport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException {
|
||||
return new ProxyTcpOverTlsTransport(server, serverCert, proxy);
|
||||
}
|
||||
},
|
||||
HTTP {
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server) {
|
||||
public CloseableTransport getTransport(HostAndPort server) {
|
||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, File serverCert) {
|
||||
public CloseableTransport getTransport(HostAndPort server, File serverCert) {
|
||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, HostAndPort proxy) {
|
||||
public CloseableTransport getTransport(HostAndPort server, HostAndPort proxy) {
|
||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) {
|
||||
public CloseableTransport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) {
|
||||
throw new UnsupportedOperationException("No transport supported for HTTP");
|
||||
}
|
||||
};
|
||||
|
||||
public abstract Transport getTransport(HostAndPort server) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
public abstract CloseableTransport getTransport(HostAndPort server) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
|
||||
public abstract Transport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
public abstract CloseableTransport getTransport(HostAndPort server, File serverCert) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
|
||||
public abstract Transport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
public abstract CloseableTransport getTransport(HostAndPort server, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
|
||||
public abstract Transport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
public abstract CloseableTransport getTransport(HostAndPort server, File serverCert, HostAndPort proxy) throws IOException, CertificateException, NoSuchAlgorithmException, KeyStoreException, KeyManagementException;
|
||||
|
||||
public HostAndPort getServerHostAndPort(String url) {
|
||||
return HostAndPort.fromString(url.substring(this.toUrlString().length()));
|
||||
}
|
||||
|
||||
public String toUrlString() {
|
||||
return toString().toLowerCase() + "://";
|
||||
return toString().toLowerCase(Locale.ROOT) + "://";
|
||||
}
|
||||
|
||||
public String toUrlString(String host) {
|
||||
@@ -117,7 +118,7 @@ public enum Protocol {
|
||||
}
|
||||
|
||||
public static boolean isOnionAddress(HostAndPort server) {
|
||||
return server.getHost().toLowerCase().endsWith(TorService.TOR_ADDRESS_SUFFIX);
|
||||
return server.getHost().toLowerCase(Locale.ROOT).endsWith(TorService.TOR_ADDRESS_SUFFIX);
|
||||
}
|
||||
|
||||
public static boolean isOnionAddress(String address) {
|
||||
|
||||
@@ -94,7 +94,7 @@ public class TcpOverTlsTransport extends TcpTransport {
|
||||
|
||||
protected void startHandshake(SSLSocket sslSocket) throws IOException {
|
||||
sslSocket.addHandshakeCompletedListener(event -> {
|
||||
if(Storage.getCertificateFile(server.getHost()) == null) {
|
||||
if(shouldSaveCertificate()) {
|
||||
try {
|
||||
Certificate[] certs = event.getPeerCertificates();
|
||||
if(certs.length > 0) {
|
||||
@@ -108,4 +108,13 @@ public class TcpOverTlsTransport extends TcpTransport {
|
||||
|
||||
sslSocket.startHandshake();
|
||||
}
|
||||
|
||||
protected boolean shouldSaveCertificate() {
|
||||
//Avoid saving the certificates for blockstream.info public servers - they change too often and encourage approval complacency
|
||||
if(PublicElectrumServer.BLOCKSTREAM_INFO.getName().equals(server.getHost()) || PublicElectrumServer.ELECTRUM_BLOCKSTREAM_INFO.getName().equals(server.getHost())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Storage.getCertificateFile(server.getHost()) == null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Condition;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
public class TcpTransport implements Transport, Closeable, TimeoutCounter {
|
||||
public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
||||
private static final Logger log = LoggerFactory.getLogger(TcpTransport.class);
|
||||
|
||||
public static final int DEFAULT_PORT = 50001;
|
||||
@@ -46,6 +46,7 @@ public class TcpTransport implements Transport, Closeable, TimeoutCounter {
|
||||
private final ReentrantLock clientRequestLock = new ReentrantLock();
|
||||
private boolean running = false;
|
||||
private volatile boolean reading = true;
|
||||
private boolean closed = false;
|
||||
private boolean firstRead = true;
|
||||
private int readTimeoutIndex;
|
||||
private int requestIdCount = 1;
|
||||
@@ -223,6 +224,7 @@ public class TcpTransport implements Transport, Closeable, TimeoutCounter {
|
||||
public void connect() throws ServerException {
|
||||
try {
|
||||
socket = createSocket();
|
||||
log.debug("Created " + socket);
|
||||
socket.setSoTimeout(SOCKET_READ_TIMEOUT_MILLIS);
|
||||
running = true;
|
||||
} catch(SSLHandshakeException e) {
|
||||
@@ -237,18 +239,23 @@ public class TcpTransport implements Transport, Closeable, TimeoutCounter {
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return socket != null && running;
|
||||
return socket != null && running && !closed;
|
||||
}
|
||||
|
||||
protected Socket createSocket() throws IOException {
|
||||
return socketFactory.createSocket(server.getHost(), server.getPortOrDefault(DEFAULT_PORT));
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return closed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if(socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
closed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -10,6 +10,9 @@ import org.slf4j.LoggerFactory;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.sparrowwallet.drongo.bip47.PaymentCode.SEGWIT_SCRIPT_TYPES;
|
||||
import static com.sparrowwallet.drongo.bip47.PaymentCode.V1_SCRIPT_TYPES;
|
||||
|
||||
public class PayNym {
|
||||
private static final Logger log = LoggerFactory.getLogger(PayNym.class);
|
||||
|
||||
@@ -68,11 +71,11 @@ public class PayNym {
|
||||
}
|
||||
|
||||
public static List<ScriptType> getSegwitScriptTypes() {
|
||||
return List.of(ScriptType.P2PKH, ScriptType.P2SH_P2WPKH, ScriptType.P2WPKH);
|
||||
return SEGWIT_SCRIPT_TYPES;
|
||||
}
|
||||
|
||||
public static List<ScriptType> getV1ScriptTypes() {
|
||||
return List.of(ScriptType.P2PKH);
|
||||
return V1_SCRIPT_TYPES;
|
||||
}
|
||||
|
||||
public static PayNym fromString(String strPaymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> followers) {
|
||||
|
||||
@@ -321,7 +321,9 @@ public class PayNymController {
|
||||
|
||||
public boolean isLinked(PayNym payNym) {
|
||||
PaymentCode externalPaymentCode = payNym.paymentCode();
|
||||
return getMasterWallet().getChildWallet(externalPaymentCode, payNym.segwit() ? ScriptType.P2WPKH : ScriptType.P2PKH) != null;
|
||||
//As per BIP47 a notification transaction must have been sent from this wallet to enable the recipient to restore funds from seed
|
||||
return getMasterWallet().getChildWallet(externalPaymentCode, payNym.segwit() ? ScriptType.P2WPKH : ScriptType.P2PKH) != null
|
||||
&& !getNotificationTransaction(externalPaymentCode).isEmpty();
|
||||
}
|
||||
|
||||
public void updateFollowing() {
|
||||
@@ -356,7 +358,7 @@ public class PayNymController {
|
||||
for(PayNym payNym : following) {
|
||||
if(!isLinked(payNym)) {
|
||||
PaymentCode externalPaymentCode = payNym.paymentCode();
|
||||
Map<BlockTransaction, WalletNode> unlinkedNotification = getMasterWallet().getNotificationTransaction(externalPaymentCode);
|
||||
Map<BlockTransaction, WalletNode> unlinkedNotification = getNotificationTransaction(externalPaymentCode);
|
||||
if(!unlinkedNotification.isEmpty() && !INVALID_PAYMENT_CODE_LABEL.equals(unlinkedNotification.keySet().iterator().next().getLabel())) {
|
||||
unlinkedNotifications.putAll(unlinkedNotification);
|
||||
unlinkedPayNyms.put(unlinkedNotification.keySet().iterator().next(), payNym);
|
||||
@@ -410,10 +412,11 @@ public class PayNymController {
|
||||
byte[] opReturnData = PaymentCode.getOpReturnData(blockTransaction.getTransaction());
|
||||
if(Arrays.equals(opReturnData, blindedPaymentCode)) {
|
||||
addedWallets.addAll(addChildWallets(payNym, externalPaymentCode));
|
||||
blockTransaction.setLabel("Link " + payNym.nymName());
|
||||
} else {
|
||||
blockTransaction.setLabel(INVALID_PAYMENT_CODE_LABEL);
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(input0Node.getWallet(), new TransactionEntry(input0Node.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap())));
|
||||
}
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(input0Node.getWallet(), new TransactionEntry(input0Node.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap())));
|
||||
} catch(Exception e) {
|
||||
log.error("Error adding linked contact from notification transaction", e);
|
||||
}
|
||||
@@ -606,6 +609,22 @@ public class PayNymController {
|
||||
return wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, opReturns, Collections.emptySet(), feeRate, minimumFeeRate, null, AppServices.getCurrentBlockHeight(), groupByAddress, includeMempoolOutputs, false);
|
||||
}
|
||||
|
||||
private Map<BlockTransaction, WalletNode> getNotificationTransaction(PaymentCode externalPaymentCode) {
|
||||
Map<BlockTransaction, WalletNode> notificationTransaction = getMasterWallet().getNotificationTransaction(externalPaymentCode);
|
||||
if(notificationTransaction.isEmpty()) {
|
||||
for(Wallet childWallet : getMasterWallet().getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
notificationTransaction = childWallet.getNotificationTransaction(externalPaymentCode);
|
||||
if(!notificationTransaction.isEmpty()) {
|
||||
return notificationTransaction;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return notificationTransaction;
|
||||
}
|
||||
|
||||
public Wallet getMasterWallet() {
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
return wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
@@ -641,6 +660,11 @@ public class PayNymController {
|
||||
if(!changedLabelEntries.isEmpty()) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelsChangedEvent(event.getWallet(), changedLabelEntries)));
|
||||
}
|
||||
|
||||
if(walletPayNym != null) {
|
||||
//If we have just linked a PayNym wallet that paid for another notification transaction, attempt to link
|
||||
Platform.runLater(() -> addWalletIfNotificationTransactionPresent(walletPayNym.following()));
|
||||
}
|
||||
}
|
||||
|
||||
public static class NoSelectionModel<T> extends MultipleSelectionModel<T> {
|
||||
|
||||
@@ -15,6 +15,7 @@ import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class PreferencesController implements Initializable {
|
||||
@@ -48,7 +49,7 @@ public class PreferencesController implements Initializable {
|
||||
}
|
||||
|
||||
PreferenceGroup preferenceGroup = (PreferenceGroup) selectedToggle.getUserData();
|
||||
String fxmlName = preferenceGroup.toString().toLowerCase();
|
||||
String fxmlName = preferenceGroup.toString().toLowerCase(Locale.ROOT);
|
||||
setPreferencePane(fxmlName);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class Soroban {
|
||||
private int bip47Account;
|
||||
|
||||
public Soroban(Network network, HostAndPort torProxy) {
|
||||
this.sorobanServer = SorobanServer.valueOf(network.getName().toUpperCase());
|
||||
this.sorobanServer = SorobanServer.valueOf(network.getName().toUpperCase(Locale.ROOT));
|
||||
this.httpClientService = new JavaHttpClientService(torProxy);
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
|
||||
public class HeadersController extends TransactionFormController implements Initializable, DynamicUpdate {
|
||||
private static final Logger log = LoggerFactory.getLogger(HeadersController.class);
|
||||
@@ -79,7 +82,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
private TransactionDiagram transactionDiagram;
|
||||
|
||||
@FXML
|
||||
private Spinner<Integer> version;
|
||||
private IntegerSpinner version;
|
||||
|
||||
@FXML
|
||||
private CopyableLabel segwit;
|
||||
@@ -109,10 +112,10 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
private Field locktimeDateField;
|
||||
|
||||
@FXML
|
||||
private Spinner<Integer> locktimeNone;
|
||||
private IntegerSpinner locktimeNone;
|
||||
|
||||
@FXML
|
||||
private Spinner<Integer> locktimeBlock;
|
||||
private IntegerSpinner locktimeBlock;
|
||||
|
||||
@FXML
|
||||
private Hyperlink locktimeCurrentHeight;
|
||||
@@ -240,8 +243,12 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
|
||||
updateTxId();
|
||||
|
||||
version.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 2, (int)tx.getVersion()));
|
||||
version.setValueFactory(new IntegerSpinner.ValueFactory(1, 2, (int)tx.getVersion()));
|
||||
version.valueProperty().addListener((obs, oldValue, newValue) -> {
|
||||
if(newValue == null || newValue < 1 || newValue > 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
tx.setVersion(newValue);
|
||||
if(oldValue != null) {
|
||||
EventManager.get().post(new TransactionChangedEvent(tx));
|
||||
@@ -302,9 +309,9 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
futureDateWarning.managedProperty().bind(futureDateWarning.visibleProperty());
|
||||
futureDateWarning.setVisible(false);
|
||||
|
||||
locktimeNone.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, 0));
|
||||
locktimeNone.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, 0));
|
||||
if(tx.getLocktime() < Transaction.MAX_BLOCK_LOCKTIME) {
|
||||
locktimeBlock.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, (int)tx.getLocktime()));
|
||||
locktimeBlock.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, (int)tx.getLocktime()));
|
||||
if(tx.getLocktime() == 0) {
|
||||
locktimeToggleGroup.selectToggle(locktimeNoneType);
|
||||
} else {
|
||||
@@ -313,13 +320,17 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
LocalDateTime date = Instant.now().atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
locktimeDate.setDateTimeValue(date);
|
||||
} else {
|
||||
locktimeBlock.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1));
|
||||
locktimeBlock.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1));
|
||||
LocalDateTime date = Instant.ofEpochSecond(tx.getLocktime()).atZone(ZoneId.systemDefault()).toLocalDateTime();
|
||||
locktimeDate.setDateTimeValue(date);
|
||||
locktimeToggleGroup.selectToggle(locktimeDateType);
|
||||
}
|
||||
|
||||
locktimeBlock.valueProperty().addListener((obs, oldValue, newValue) -> {
|
||||
if(newValue == null || newValue < 0 || newValue >= Transaction.MAX_BLOCK_LOCKTIME) {
|
||||
return;
|
||||
}
|
||||
|
||||
tx.setLocktime(newValue);
|
||||
locktimeCurrentHeight.setVisible(headersForm.isEditable() && AppServices.getCurrentBlockHeight() != null && newValue < AppServices.getCurrentBlockHeight());
|
||||
futureBlockWarning.setVisible(AppServices.getCurrentBlockHeight() != null && newValue > AppServices.getCurrentBlockHeight());
|
||||
@@ -473,16 +484,28 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
noWalletsWarningLink.managedProperty().bind(noWalletsWarningLink.visibleProperty());
|
||||
noWalletsWarningLink.visibleProperty().bind(noWalletsWarning.visibleProperty());
|
||||
|
||||
SigHash psbtSigHash = SigHash.ALL;
|
||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||
if(psbtInput.getSigHash() != null) {
|
||||
psbtSigHash = psbtInput.getSigHash();
|
||||
boolean taprootInput = psbt.getPsbtInputs().stream().anyMatch(PSBTInput::isTaproot);
|
||||
SigHash psbtSigHash = psbt.getPsbtInputs().stream().map(PSBTInput::getSigHash).filter(Objects::nonNull).findFirst().orElse(taprootInput ? SigHash.DEFAULT : SigHash.ALL);
|
||||
sigHash.setItems(FXCollections.observableList(taprootInput ? SigHash.TAPROOT_SIGNING_TYPES : SigHash.LEGACY_SIGNING_TYPES));
|
||||
sigHash.setValue(psbtSigHash);
|
||||
sigHash.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(SigHash sigHash) {
|
||||
if(sigHash == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return sigHash.getName() + ((taprootInput && sigHash == SigHash.DEFAULT) || (!taprootInput && sigHash == SigHash.ALL) ? " (Recommended)" : "");
|
||||
}
|
||||
}
|
||||
sigHash.setValue(psbtSigHash == SigHash.ALL_TAPROOT ? SigHash.ALL : psbtSigHash);
|
||||
|
||||
@Override
|
||||
public SigHash fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
sigHash.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||
psbtInput.setSigHash(psbtInput.isTaproot() && newValue == SigHash.ALL ? SigHash.ALL_TAPROOT : newValue);
|
||||
psbtInput.setSigHash(newValue == SigHash.DEFAULT && !psbtInput.isTaproot() ? SigHash.ALL : newValue);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -729,7 +752,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
|
||||
private void initializeSignButton(Wallet signingWallet) {
|
||||
Optional<Keystore> softwareKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_SEED)).findAny();
|
||||
Optional<Keystore> usbKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB)).findAny();
|
||||
Optional<Keystore> usbKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB) || keystore.getSource().equals(KeystoreSource.SW_WATCH)).findAny();
|
||||
Optional<Keystore> bip47Keystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_PAYMENT_CODE)).findAny();
|
||||
if(softwareKeystore.isEmpty() && usbKeystore.isEmpty() && bip47Keystore.isEmpty()) {
|
||||
signButton.setDisable(true);
|
||||
@@ -794,7 +817,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
|
||||
public void copyId(ActionEvent event) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(headersForm.getTransaction().getTxId().toString());
|
||||
content.putString(headersForm.getTransaction().calculateTxId(false).toString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
}
|
||||
|
||||
@@ -829,7 +852,49 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
ToggleButton toggleButton = (ToggleButton)event.getSource();
|
||||
toggleButton.setSelected(false);
|
||||
|
||||
EventManager.get().post(new RequestQRScanEvent(toggleButton.getScene().getWindow()));
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||
if(optionalResult.isPresent()) {
|
||||
QRScanDialog.Result result = optionalResult.get();
|
||||
if(result.transaction != null) {
|
||||
EventManager.get().post(new ViewTransactionEvent(toggleButton.getScene().getWindow(), result.transaction));
|
||||
} else if(result.psbt != null) {
|
||||
EventManager.get().post(new ViewPSBTEvent(toggleButton.getScene().getWindow(), null, null, result.psbt));
|
||||
} else if(result.seed != null) {
|
||||
signFromSeed(result.seed);
|
||||
} else if(result.exception != null) {
|
||||
log.error("Error scanning QR", result.exception);
|
||||
showErrorDialog("Error scanning QR", result.exception.getMessage());
|
||||
} else {
|
||||
AppServices.showErrorDialog("Invalid QR Code", "Cannot parse QR code into a transaction or seed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void signFromSeed(DeterministicSeed seed) {
|
||||
try {
|
||||
String masterFingerprint = Keystore.fromSeed(seed, ScriptType.P2PKH.getDefaultDerivation()).getKeyDerivation().getMasterFingerprint();
|
||||
Wallet walletCopy = headersForm.getSigningWallet().copy();
|
||||
OptionalInt optIndex = IntStream.range(0, walletCopy.getKeystores().size())
|
||||
.filter(i -> walletCopy.getKeystores().get(i).getKeyDerivation().getMasterFingerprint().equals(masterFingerprint)).findFirst();
|
||||
if(optIndex.isPresent()) {
|
||||
walletCopy.getKeystores().forEach(keystore -> {
|
||||
keystore.setSeed(null);
|
||||
keystore.setMasterPrivateExtendedKey(null);
|
||||
});
|
||||
Keystore original = walletCopy.getKeystores().get(optIndex.getAsInt());
|
||||
Keystore replacement = Keystore.fromSeed(seed, original.getKeyDerivation().getDerivation());
|
||||
walletCopy.getKeystores().set(optIndex.getAsInt(), replacement);
|
||||
signUnencryptedKeystores(walletCopy);
|
||||
} else {
|
||||
AppServices.showErrorDialog("Invalid seed", "The QR code contains a seed that does not match any of the keystores in the signing wallet.");
|
||||
}
|
||||
} catch(MnemonicException e) {
|
||||
log.error("Invalid seed", e);
|
||||
AppServices.showErrorDialog("Invalid seed", e.getMessage());
|
||||
} finally {
|
||||
seed.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void savePSBT(ActionEvent event) {
|
||||
@@ -848,7 +913,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
if(!file.getName().toLowerCase().endsWith(".psbt")) {
|
||||
if(!file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt")) {
|
||||
file = new File(file.getAbsolutePath() + ".psbt");
|
||||
}
|
||||
|
||||
@@ -924,7 +989,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
|
||||
List<String> fingerprints = headersForm.getSigningWallet().getKeystores().stream().map(keystore -> keystore.getKeyDerivation().getMasterFingerprint()).collect(Collectors.toList());
|
||||
List<Device> signingDevices = AppServices.getDevices().stream().filter(device -> fingerprints.contains(device.getFingerprint())).collect(Collectors.toList());
|
||||
if(signingDevices.isEmpty() && headersForm.getSigningWallet().getKeystores().stream().noneMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB))) {
|
||||
if(signingDevices.isEmpty() && headersForm.getSigningWallet().getKeystores().stream().noneMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB) || keystore.getSource().equals(KeystoreSource.SW_WATCH))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1042,7 +1107,14 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
broadcastTransactionService.setOnFailed(workerStateEvent -> {
|
||||
broadcastProgressBar.setProgress(0);
|
||||
log.error("Error broadcasting transaction", workerStateEvent.getSource().getException());
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The server returned an error when broadcasting the transaction. The server response is contained in the log (See Help > Show Log File).");
|
||||
if(workerStateEvent.getSource().getException() != null && workerStateEvent.getSource().getException().getMessage() != null
|
||||
&& workerStateEvent.getSource().getException().getMessage().startsWith("min relay fee not met")) {
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The fee rate for the signed transaction is below the minimum " + AppServices.getMinimumRelayFeeRate() + " sats/vB. " +
|
||||
"This usually happens because a keystore has created a signature that is larger than necessary.\n\n" +
|
||||
"You can solve this by recreating the transaction with a slightly increased fee rate.");
|
||||
} else {
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", "The server returned an error when broadcasting the transaction. The server response is contained in the log (See Help > Show Log File).");
|
||||
}
|
||||
broadcastButton.setDisable(false);
|
||||
});
|
||||
|
||||
@@ -1165,16 +1237,18 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
}
|
||||
}
|
||||
|
||||
Long feeAmt = calculateFee(event.getInputTransactions());
|
||||
if(feeAmt != null) {
|
||||
updateFee(feeAmt);
|
||||
}
|
||||
if(!event.getInputTransactions().isEmpty()) {
|
||||
Long feeAmt = calculateFee(event.getInputTransactions());
|
||||
if(feeAmt != null) {
|
||||
updateFee(feeAmt);
|
||||
}
|
||||
|
||||
Map<Sha256Hash, BlockTransaction> allFetchedInputTransactions = new HashMap<>(event.getInputTransactions());
|
||||
if(headersForm.getInputTransactions() != null) {
|
||||
allFetchedInputTransactions.putAll(headersForm.getInputTransactions());
|
||||
Map<Sha256Hash, BlockTransaction> allFetchedInputTransactions = new HashMap<>(event.getInputTransactions());
|
||||
if(headersForm.getInputTransactions() != null) {
|
||||
allFetchedInputTransactions.putAll(headersForm.getInputTransactions());
|
||||
}
|
||||
transactionDiagram.update(getWalletTransaction(allFetchedInputTransactions));
|
||||
}
|
||||
transactionDiagram.update(getWalletTransaction(allFetchedInputTransactions));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1387,6 +1461,12 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
if(!changedLabelEntries.isEmpty()) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelsChangedEvent(event.getWallet(), changedLabelEntries)));
|
||||
}
|
||||
} else if(headersForm.getBlockTransaction() != null && headersForm.getBlockTransaction().getHeight() <= 0) {
|
||||
BlockTransaction walletTransaction = event.getWallet().getWalletTransaction(headersForm.getBlockTransaction().getHash());
|
||||
if(walletTransaction != null && walletTransaction.getHeight() > 0) {
|
||||
headersForm.setBlockTransaction(walletTransaction);
|
||||
updateBlockchainForm(headersForm.getBlockTransaction(), AppServices.getCurrentBlockHeight());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ public class InputController extends TransactionFormController implements Initia
|
||||
private CopyableLabel locktimeAbsolute;
|
||||
|
||||
@FXML
|
||||
private Spinner<Integer> locktimeRelativeBlocks;
|
||||
private IntegerSpinner locktimeRelativeBlocks;
|
||||
|
||||
@FXML
|
||||
private RelativeTimelockSpinner locktimeRelativeSeconds;
|
||||
@@ -406,7 +406,7 @@ public class InputController extends TransactionFormController implements Initia
|
||||
}
|
||||
});
|
||||
|
||||
locktimeRelativeBlocks.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK, 0));
|
||||
locktimeRelativeBlocks.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK, 0));
|
||||
locktimeRelativeBlocks.managedProperty().bind(locktimeRelativeBlocks.visibleProperty());
|
||||
locktimeRelativeSeconds.managedProperty().bind(locktimeRelativeSeconds.visibleProperty());
|
||||
locktimeRelativeCombo.getSelectionModel().selectedItemProperty().addListener((ov, old_toggle, new_toggle) -> {
|
||||
@@ -433,6 +433,10 @@ public class InputController extends TransactionFormController implements Initia
|
||||
}
|
||||
|
||||
locktimeRelativeBlocks.valueProperty().addListener((obs, oldValue, newValue) -> {
|
||||
if(newValue == null || newValue < 0 || newValue > TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRelativeLocktime(txInput, transaction, oldValue != null);
|
||||
});
|
||||
locktimeRelativeSeconds.valueProperty().addListener((obs, oldValue, newValue) -> {
|
||||
@@ -509,7 +513,7 @@ public class InputController extends TransactionFormController implements Initia
|
||||
|
||||
@Subscribe
|
||||
public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
|
||||
if(event.getTxId().equals(inputForm.getTransaction().getTxId()) && inputForm.getIndex() >= event.getPageStart() && inputForm.getIndex() < event.getPageEnd()) {
|
||||
if(event.getTxId().equals(inputForm.getTransaction().getTxId()) && !event.getInputTransactions().isEmpty() && inputForm.getIndex() >= event.getPageStart() && inputForm.getIndex() < event.getPageEnd()) {
|
||||
updateOutpoint(event.getInputTransactions());
|
||||
if(inputForm.getPsbt() == null) {
|
||||
updateSpends(event.getInputTransactions());
|
||||
|
||||
@@ -164,7 +164,7 @@ public class InputsController extends TransactionFormController implements Initi
|
||||
|
||||
@Subscribe
|
||||
public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
|
||||
if(event.getTxId().equals(inputsForm.getTransaction().getTxId()) && inputsForm.getPsbt() == null) {
|
||||
if(event.getTxId().equals(inputsForm.getTransaction().getTxId()) && !event.getInputTransactions().isEmpty() && inputsForm.getPsbt() == null) {
|
||||
updateBlockTransactionInputs(event.getInputTransactions());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTOutput;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.TransactionTabData;
|
||||
@@ -30,6 +31,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TransactionController implements Initializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(TransactionController.class);
|
||||
@@ -57,6 +59,8 @@ public class TransactionController implements Initializable {
|
||||
private int selectedInputIndex = -1;
|
||||
private int selectedOutputIndex = -1;
|
||||
|
||||
private boolean transactionsFetched;
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
@@ -68,6 +72,16 @@ public class TransactionController implements Initializable {
|
||||
transactionMasterDetail.setShowDetailNode(Config.get().isShowTransactionHex());
|
||||
txhex.setTransaction(getTransaction());
|
||||
highlightTxHex();
|
||||
fetchTransactions();
|
||||
|
||||
transactionMasterDetail.sceneProperty().addListener((observable, oldScene, newScene) -> {
|
||||
if(oldScene == null && newScene != null) {
|
||||
transactionMasterDetail.setDividerPosition(AppServices.isReducedWindowHeight(transactionMasterDetail) ? 0.9 : 0.82);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void fetchTransactions() {
|
||||
fetchThisAndInputBlockTransactions(0, Math.min(getTransaction().getInputs().size(), PageForm.PAGE_SIZE));
|
||||
fetchOutputBlockTransactions(0, Math.min(getTransaction().getOutputs().size(), PageForm.PAGE_SIZE));
|
||||
|
||||
@@ -76,12 +90,6 @@ public class TransactionController implements Initializable {
|
||||
} else if(TransactionView.OUTPUT.equals(initialView) && initialIndex >= PageForm.PAGE_SIZE) {
|
||||
fetchOutputBlockTransactions(initialIndex, initialIndex + 1);
|
||||
}
|
||||
|
||||
transactionMasterDetail.sceneProperty().addListener((observable, oldScene, newScene) -> {
|
||||
if(oldScene == null && newScene != null) {
|
||||
transactionMasterDetail.setDividerPosition(AppServices.isReducedWindowHeight(transactionMasterDetail) ? 0.9 : 0.82);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeTxTree() {
|
||||
@@ -353,6 +361,7 @@ public class TransactionController implements Initializable {
|
||||
|
||||
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(references);
|
||||
transactionReferenceService.setOnSucceeded(successEvent -> {
|
||||
transactionsFetched = true;
|
||||
Map<Sha256Hash, BlockTransaction> transactionMap = transactionReferenceService.getValue();
|
||||
BlockTransaction thisBlockTx = null;
|
||||
Map<Sha256Hash, BlockTransaction> inputTransactions = new HashMap<>();
|
||||
@@ -387,6 +396,28 @@ public class TransactionController implements Initializable {
|
||||
});
|
||||
EventManager.get().post(new TransactionReferencesStartedEvent(getTransaction(), indexStart, maxIndex));
|
||||
transactionReferenceService.start();
|
||||
} else if(!AppServices.isConnected()) {
|
||||
BlockTransaction blockTx = null;
|
||||
Set<Sha256Hash> inputReferences = getTransaction().getInputs().stream().map(input -> input.getOutpoint().getHash()).collect(Collectors.toSet());
|
||||
Map<Sha256Hash, BlockTransaction> inputTransactions = new HashMap<>();
|
||||
|
||||
for(Wallet wallet : AppServices.get().getOpenWallets().keySet()) {
|
||||
Map<Sha256Hash, BlockTransaction> walletTransactions = wallet.getWalletTransactions();
|
||||
if(blockTx == null && walletTransactions.get(getTransaction().getTxId()) != null) {
|
||||
blockTx = walletTransactions.get(getTransaction().getTxId());
|
||||
}
|
||||
for(Sha256Hash inputReference : inputReferences) {
|
||||
if(inputTransactions.get(inputReference) == null && walletTransactions.get(inputReference) != null) {
|
||||
inputTransactions.put(inputReference, walletTransactions.get(inputReference));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(inputTransactions.size() == inputReferences.size()) {
|
||||
EventManager.get().post(new BlockTransactionFetchedEvent(getTransaction(), blockTx, inputTransactions, 0, getTransaction().getInputs().size()));
|
||||
} else {
|
||||
EventManager.get().post(new BlockTransactionFetchedEvent(getTransaction(), blockTx, Collections.emptyMap(), 0, getTransaction().getInputs().size()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,7 +463,7 @@ public class TransactionController implements Initializable {
|
||||
|
||||
@Subscribe
|
||||
public void viewTransaction(ViewTransactionEvent event) {
|
||||
if(txdata.getTransaction().getTxId().equals(event.getBlockTransaction().getTransaction().getTxId())) {
|
||||
if(txdata.getTransaction().getTxId().equals(event.getTransaction().getTxId())) {
|
||||
TreeItem<TransactionForm> existingItem = getTreeItem(event.getInitialView(), event.getInitialIndex());
|
||||
if(existingItem != null && !(existingItem.getValue() instanceof PageForm)) {
|
||||
setTreeSelection(event.getInitialView(), event.getInitialIndex());
|
||||
@@ -500,7 +531,7 @@ public class TransactionController implements Initializable {
|
||||
|
||||
@Subscribe
|
||||
public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
|
||||
if(event.getTxId().equals(getTransaction().getTxId())) {
|
||||
if(event.getTxId().equals(getTransaction().getTxId()) && !event.getInputTransactions().isEmpty()) {
|
||||
if(event.getBlockTransaction() != null && (!Sha256Hash.ZERO_HASH.equals(event.getBlockTransaction().getBlockHash()) || txdata.getBlockTransaction() == null)) {
|
||||
txdata.setBlockTransaction(event.getBlockTransaction());
|
||||
}
|
||||
@@ -547,4 +578,18 @@ public class TransactionController implements Initializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newConnection(ConnectionEvent event) {
|
||||
if(!transactionsFetched) {
|
||||
fetchTransactions();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void openWallets(OpenWalletsEvent event) {
|
||||
if(!transactionsFetched) {
|
||||
fetchTransactions();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
@@ -125,6 +126,12 @@ public class AddressesController extends WalletFormController implements Initial
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void showTransactionsCount(ShowTransactionsCountEvent event) {
|
||||
receiveTable.showTransactionsCount(event.isShowCount());
|
||||
changeTable.showTransactionsCount(event.isShowCount());
|
||||
}
|
||||
|
||||
public void exportReceiveAddresses(ActionEvent event) {
|
||||
exportAddresses(KeyPurpose.RECEIVE);
|
||||
}
|
||||
@@ -138,7 +145,7 @@ public class AddressesController extends WalletFormController implements Initial
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Export Addresses to CSV");
|
||||
fileChooser.setInitialFileName(getWalletForm().getWallet().getFullName() + "-" + keyPurpose.name().toLowerCase() + "-addresses.csv");
|
||||
fileChooser.setInitialFileName(getWalletForm().getWallet().getFullName() + "-" + keyPurpose.name().toLowerCase(Locale.ROOT) + "-addresses.csv");
|
||||
|
||||
Wallet copy = getWalletForm().getWallet().copy();
|
||||
WalletNode purposeNode = copy.getNode(keyPurpose);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.wallet;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.DateStringConverter;
|
||||
import com.sparrowwallet.sparrow.control.IntegerSpinner;
|
||||
import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
@@ -10,8 +11,6 @@ import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.DatePicker;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.control.SpinnerValueFactory;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.net.URL;
|
||||
@@ -28,7 +27,7 @@ public class AdvancedController implements Initializable {
|
||||
private DatePicker birthDate;
|
||||
|
||||
@FXML
|
||||
private Spinner<Integer> gapLimit;
|
||||
private IntegerSpinner gapLimit;
|
||||
|
||||
@FXML
|
||||
private ComboBox<Integer> watchLast;
|
||||
@@ -50,8 +49,12 @@ public class AdvancedController implements Initializable {
|
||||
}
|
||||
});
|
||||
|
||||
gapLimit.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(Wallet.DEFAULT_LOOKAHEAD, 9999, wallet.getGapLimit()));
|
||||
gapLimit.setValueFactory(new IntegerSpinner.ValueFactory(Wallet.DEFAULT_LOOKAHEAD, 9999, wallet.getGapLimit()));
|
||||
gapLimit.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue == null || newValue < Wallet.DEFAULT_LOOKAHEAD || newValue > 9999) {
|
||||
return;
|
||||
}
|
||||
|
||||
wallet.setGapLimit(newValue);
|
||||
if(!watchLast.getItems().equals(getWatchListItems(wallet))) {
|
||||
Integer value = watchLast.getValue();
|
||||
@@ -80,6 +83,10 @@ public class AdvancedController implements Initializable {
|
||||
});
|
||||
}
|
||||
|
||||
public void close() {
|
||||
gapLimit.commitValue();
|
||||
}
|
||||
|
||||
private ObservableList<Integer> getWatchListItems(Wallet wallet) {
|
||||
return FXCollections.observableList(DEFAULT_WATCH_LIST_ITEMS.stream().filter(val -> val < 0 || val > wallet.getGapLimit()).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ public class AdvancedDialog extends Dialog<Boolean> {
|
||||
dialogPane.setPrefHeight(300);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
settingsAdvancedController.close();
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == passwordButtonType);
|
||||
}
|
||||
catch(IOException e) {
|
||||
|
||||
@@ -2,10 +2,7 @@ package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
@@ -241,7 +238,9 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
|
||||
validationSupport.registerValidator(xpub, Validator.combine(
|
||||
Validator.createEmptyValidator(Network.get().getXpubHeader().getDisplayName() + " is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getXpubHeader().getDisplayName() + " is invalid", !ExtendedKey.isValid(newValue))
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getXpubHeader().getDisplayName() + " is invalid", !ExtendedKey.isValid(newValue)),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Extended key is not unique", ExtendedKey.isValid(newValue) &&
|
||||
walletForm.getWallet().getKeystores().stream().filter(k -> k != keystore && k.getExtendedPublicKey() != null).map(Keystore::getExtendedPublicKey).collect(Collectors.toList()).contains(ExtendedKey.fromDescriptor(newValue)))
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(derivation, Validator.combine(
|
||||
@@ -413,6 +412,18 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
}
|
||||
|
||||
AppServices.showErrorDialog("Missing Script Type", "QR Code did not contain any information for the " + getWalletForm().getWallet().getScriptType().getDescription() + " script type.");
|
||||
} else if(result.seed != null) {
|
||||
try {
|
||||
Keystore keystore = Keystore.fromSeed(result.seed, getWalletForm().getWallet().getScriptType().getDefaultDerivation());
|
||||
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
|
||||
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
|
||||
xpub.setText(keystore.getExtendedPublicKey().toString());
|
||||
} catch(MnemonicException e) {
|
||||
log.error("Error parsing seed", e);
|
||||
AppServices.showErrorDialog("Error parsing seed", e.getMessage());
|
||||
} finally {
|
||||
result.seed.clear();
|
||||
}
|
||||
} else if(result.exception != null) {
|
||||
log.error("Error scanning QR", result.exception);
|
||||
AppServices.showErrorDialog("Error scanning QR", result.exception.getMessage());
|
||||
|
||||
@@ -7,23 +7,21 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.IntegerSpinner;
|
||||
import com.sparrowwallet.sparrow.event.MixToConfigChangedEvent;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.control.SpinnerValueFactory;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
|
||||
|
||||
public class MixToController implements Initializable {
|
||||
private static final Wallet NONE_WALLET = new Wallet("None");
|
||||
|
||||
@@ -31,7 +29,7 @@ public class MixToController implements Initializable {
|
||||
private ComboBox<Wallet> mixToWallets;
|
||||
|
||||
@FXML
|
||||
private Spinner<Integer> minMixes;
|
||||
private IntegerSpinner minMixes;
|
||||
|
||||
@FXML
|
||||
private ComboBox<IndexRange> indexRange;
|
||||
@@ -52,7 +50,7 @@ public class MixToController implements Initializable {
|
||||
List<Wallet> destinationWallets = AppServices.get().getOpenWallets().keySet().stream().filter(openWallet -> openWallet.isValid()
|
||||
&& (openWallet.getScriptType() == ScriptType.P2WPKH || openWallet.getScriptType() == ScriptType.P2WSH)
|
||||
&& openWallet != wallet && openWallet != wallet.getMasterWallet()
|
||||
&& (openWallet.getStandardAccountType() == null || !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(openWallet.getStandardAccountType()))).collect(Collectors.toList());
|
||||
&& (openWallet.getStandardAccountType() == null || !List.of(WHIRLPOOL_PREMIX, WHIRLPOOL_BADBANK).contains(openWallet.getStandardAccountType()))).collect(Collectors.toList());
|
||||
allWallets.addAll(destinationWallets);
|
||||
|
||||
mixToWallets.setItems(FXCollections.observableList(allWallets));
|
||||
@@ -94,8 +92,12 @@ public class MixToController implements Initializable {
|
||||
});
|
||||
|
||||
int initialMinMixes = mixConfig.getMinMixes() == null ? Whirlpool.DEFAULT_MIXTO_MIN_MIXES : mixConfig.getMinMixes();
|
||||
minMixes.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 10000, initialMinMixes));
|
||||
minMixes.setValueFactory(new IntegerSpinner.ValueFactory(2, 10000, initialMinMixes));
|
||||
minMixes.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue == null || newValue < 2 || newValue > 10000) {
|
||||
return;
|
||||
}
|
||||
|
||||
mixConfig.setMinMixes(newValue);
|
||||
EventManager.get().post(new MixToConfigChangedEvent(wallet));
|
||||
});
|
||||
@@ -107,7 +109,7 @@ public class MixToController implements Initializable {
|
||||
return "";
|
||||
}
|
||||
|
||||
return indexRange.toString().charAt(0) + indexRange.toString().substring(1).toLowerCase();
|
||||
return indexRange.toString().charAt(0) + indexRange.toString().substring(1).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,6 +132,10 @@ public class MixToController implements Initializable {
|
||||
});
|
||||
}
|
||||
|
||||
public void close() {
|
||||
minMixes.commitValue();
|
||||
}
|
||||
|
||||
public MixConfig getMixConfig() {
|
||||
return mixConfig;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ public class MixToDialog extends Dialog<MixConfig> {
|
||||
setResultConverter(dialogButton -> dialogButton == applyButtonType ? mixToController.getMixConfig() : null);
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
mixToController.close();
|
||||
EventManager.get().unregister(this);
|
||||
});
|
||||
EventManager.get().register(this);
|
||||
|
||||
@@ -312,7 +312,9 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
}
|
||||
|
||||
private void updateOpenWallets(Collection<Wallet> wallets) {
|
||||
List<Wallet> openWalletList = wallets.stream().filter(wallet -> wallet.isValid() && !wallet.isWhirlpoolChildWallet() && !wallet.isBip47()).collect(Collectors.toList());
|
||||
List<Wallet> openWalletList = wallets.stream().filter(wallet -> wallet.isValid()
|
||||
&& (wallet == sendController.getWalletForm().getWallet() || !wallet.isWhirlpoolChildWallet())
|
||||
&& !wallet.isBip47()).collect(Collectors.toList());
|
||||
|
||||
if(sendController.getWalletForm().getWallet().hasPaymentCode()) {
|
||||
openWalletList.add(payNymWallet);
|
||||
@@ -497,6 +499,12 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
try {
|
||||
AppServices.clearPayjoinURI(getRecipientAddress());
|
||||
} catch(InvalidAddressException e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
address.setText("");
|
||||
label.setText("");
|
||||
|
||||
|
||||
@@ -10,10 +10,7 @@ import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.bip47.SecretPoint;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutPoint;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@@ -178,6 +175,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
|
||||
private final Set<WalletNode> excludedChangeNodes = new HashSet<>();
|
||||
|
||||
private final Map<Wallet, Map<Address, WalletNode>> addressNodeMap = new HashMap<>();
|
||||
|
||||
private final ChangeListener<String> feeListener = new ChangeListener<>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
|
||||
@@ -400,6 +399,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
if(userFeeSet.get()) {
|
||||
setTargetBlocks(getTargetBlocks(feeRate));
|
||||
setFeeRangeRate(feeRate);
|
||||
revalidate(fee, feeListener);
|
||||
} else {
|
||||
setFeeValueSats(walletTransaction.getFee());
|
||||
}
|
||||
@@ -577,7 +577,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
|
||||
boolean includeSpentMempoolOutputs = includeSpentMempoolOutputsProperty.get();
|
||||
|
||||
walletTransactionService = new WalletTransactionService(wallet, getUtxoSelectors(payments), getUtxoFilters(), payments, opReturnsList, excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
|
||||
walletTransactionService = new WalletTransactionService(addressNodeMap, wallet, getUtxoSelectors(payments), getUtxoFilters(),
|
||||
payments, opReturnsList, excludedChangeNodes,
|
||||
feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
|
||||
walletTransactionService.setOnSucceeded(event -> {
|
||||
if(!walletTransactionService.isIgnoreResult()) {
|
||||
walletTransactionProperty.setValue(walletTransactionService.getValue());
|
||||
@@ -597,7 +599,12 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
final WalletTransactionService currentWalletTransactionService = walletTransactionService;
|
||||
final KeyFrame delay = new KeyFrame(Duration.millis(200), e -> {
|
||||
if(currentWalletTransactionService.isRunning()) {
|
||||
transactionDiagram.update("Selecting UTXOs...");
|
||||
transactionDiagram.update(currentWalletTransactionService.getMessage());
|
||||
currentWalletTransactionService.messageProperty().addListener((observable1, oldValue, newValue) -> {
|
||||
if(currentWalletTransactionService.isRunning()) {
|
||||
transactionDiagram.update(newValue);
|
||||
}
|
||||
});
|
||||
createButton.setDisable(true);
|
||||
notificationButton.setDisable(true);
|
||||
}
|
||||
@@ -635,6 +642,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
|
||||
private static class WalletTransactionService extends Service<WalletTransaction> {
|
||||
private final Map<Wallet, Map<Address, WalletNode>> addressNodeMap;
|
||||
private final Wallet wallet;
|
||||
private final List<UtxoSelector> utxoSelectors;
|
||||
private final List<UtxoFilter> utxoFilters;
|
||||
@@ -650,7 +658,11 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
private final boolean includeSpentMempoolOutputs;
|
||||
private boolean ignoreResult;
|
||||
|
||||
public WalletTransactionService(Wallet wallet, List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters, List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs) {
|
||||
public WalletTransactionService(Map<Wallet, Map<Address, WalletNode>> addressNodeMap,
|
||||
Wallet wallet, List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters,
|
||||
List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes,
|
||||
double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs) {
|
||||
this.addressNodeMap = addressNodeMap;
|
||||
this.wallet = wallet;
|
||||
this.utxoSelectors = utxoSelectors;
|
||||
this.utxoFilters = utxoFilters;
|
||||
@@ -670,7 +682,12 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
protected Task<WalletTransaction> createTask() {
|
||||
return new Task<>() {
|
||||
protected WalletTransaction call() throws InsufficientFundsException {
|
||||
return wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, opReturns, excludedChangeNodes, feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
|
||||
updateMessage("Selecting UTXOs...");
|
||||
WalletTransaction walletTransaction = wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, opReturns, excludedChangeNodes,
|
||||
feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
|
||||
updateMessage("Deriving keys...");
|
||||
walletTransaction.updateAddressNodeMap(addressNodeMap, walletTransaction.getWallet());
|
||||
return walletTransaction;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -985,7 +1002,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
private boolean isMixPossible(List<Payment> payments) {
|
||||
return (utxoSelectorProperty.get() == null || SorobanServices.canWalletMix(walletForm.getWallet()))
|
||||
&& payments.size() == 1
|
||||
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType())
|
||||
&& AppServices.getPayjoinURI(payments.get(0).getAddress()) == null;
|
||||
}
|
||||
|
||||
private void updateOptimizationButtons(List<Payment> payments) {
|
||||
@@ -1076,6 +1094,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
|
||||
whirlpoolProperty.set(null);
|
||||
paymentCodeProperty.set(null);
|
||||
|
||||
addressNodeMap.clear();
|
||||
}
|
||||
|
||||
public UtxoSelector getUtxoSelector() {
|
||||
@@ -1153,8 +1173,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
WalletTransaction walletTransaction = walletTransactionProperty.get();
|
||||
Set<WalletNode> nodes = new LinkedHashSet<>(walletTransaction.getSelectedUtxos().values());
|
||||
nodes.addAll(walletTransaction.getChangeMap().keySet());
|
||||
List<WalletNode> consolidationNodes = walletTransaction.getConsolidationSendNodes();
|
||||
nodes.addAll(consolidationNodes);
|
||||
Map<Address, WalletNode> addressNodeMap = walletTransaction.getAddressNodeMap();
|
||||
nodes.addAll(addressNodeMap.values().stream().filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
|
||||
//All wallet nodes applicable to this transaction are stored so when the subscription status for one is updated, the history for all can be fetched in one atomic update
|
||||
walletForm.addWalletTransactionNodes(nodes);
|
||||
@@ -1621,18 +1641,19 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
|
||||
private class PrivacyAnalysisTooltip extends VBox {
|
||||
private List<Label> analysisLabels = new ArrayList<>();
|
||||
private final List<Label> analysisLabels = new ArrayList<>();
|
||||
|
||||
public PrivacyAnalysisTooltip(WalletTransaction walletTransaction) {
|
||||
List<Payment> payments = walletTransaction.getPayments();
|
||||
List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
||||
Map<Address, WalletNode> walletAddresses = getWalletForm().getWallet().getWalletAddresses();
|
||||
Map<Address, WalletNode> walletAddresses = walletTransaction.getAddressNodeMap();
|
||||
OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy();
|
||||
boolean payNymPresent = isPayNymMixOnlyPayment(payments);
|
||||
boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX);
|
||||
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0);
|
||||
boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||
boolean addressReuse = userPayments.stream().anyMatch(payment -> walletAddresses.get(payment.getAddress()) != null && !walletAddresses.get(payment.getAddress()).getTransactionOutputs().isEmpty());
|
||||
boolean payjoinPresent = userPayments.stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
|
||||
|
||||
if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
|
||||
if(payNymPresent) {
|
||||
@@ -1644,6 +1665,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
addLabel("Cannot coinjoin due to mixed address types", getInfoGlyph());
|
||||
} else if(userPayments.size() > 1) {
|
||||
addLabel("Cannot coinjoin due to multiple payments", getInfoGlyph());
|
||||
} else if(payjoinPresent) {
|
||||
addLabel("Cannot coinjoin due to payjoin", getInfoGlyph());
|
||||
} else {
|
||||
if(utxoSelectorProperty().get() != null) {
|
||||
addLabel("Cannot fake coinjoin due to coin control", getInfoGlyph());
|
||||
|
||||
@@ -471,7 +471,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
}
|
||||
|
||||
private void addAccounts(Wallet masterWallet, List<StandardAccount> standardAccounts, boolean discoverAccounts) {
|
||||
if(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)) {
|
||||
if(masterWallet.getKeystores().stream().anyMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)) {
|
||||
if(masterWallet.isEncrypted()) {
|
||||
String walletId = walletForm.getWalletId();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(masterWallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
@@ -485,7 +485,12 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
encryptionFullKey.clear();
|
||||
masterWallet.decrypt(key);
|
||||
|
||||
if(discoverAccounts) {
|
||||
if(masterWallet.getKeystores().stream().anyMatch(ks -> ks.getSource() != KeystoreSource.SW_SEED)) {
|
||||
for(StandardAccount standardAccount : standardAccounts) {
|
||||
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
||||
}
|
||||
} else if(discoverAccounts) {
|
||||
ElectrumServer.AccountDiscoveryService accountDiscoveryService = new ElectrumServer.AccountDiscoveryService(masterWallet, standardAccounts);
|
||||
accountDiscoveryService.setOnSucceeded(event -> {
|
||||
addAndEncryptAccounts(masterWallet, accountDiscoveryService.getValue(), key);
|
||||
@@ -518,7 +523,12 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
if(discoverAccounts) {
|
||||
if(masterWallet.getKeystores().stream().anyMatch(ks -> ks.getSource() != KeystoreSource.SW_SEED)) {
|
||||
for(StandardAccount standardAccount : standardAccounts) {
|
||||
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
||||
}
|
||||
} else if(discoverAccounts) {
|
||||
ElectrumServer.AccountDiscoveryService accountDiscoveryService = new ElectrumServer.AccountDiscoveryService(masterWallet, standardAccounts);
|
||||
accountDiscoveryService.setOnSucceeded(event -> {
|
||||
addAndSaveAccounts(masterWallet, accountDiscoveryService.getValue());
|
||||
|
||||
@@ -26,6 +26,7 @@ public class SettingsWalletForm extends WalletForm {
|
||||
public SettingsWalletForm(Storage storage, Wallet currentWallet) {
|
||||
super(storage, currentWallet, false);
|
||||
this.walletCopy = currentWallet.copy();
|
||||
this.walletCopy.setMasterWallet(walletCopy.isMasterWallet() ? null : walletCopy.getMasterWallet().copy());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -41,6 +42,7 @@ public class SettingsWalletForm extends WalletForm {
|
||||
@Override
|
||||
public void revert() {
|
||||
this.walletCopy = super.getWallet().copy();
|
||||
this.walletCopy.setMasterWallet(walletCopy.isMasterWallet() ? null : walletCopy.getMasterWallet().copy());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,18 +84,19 @@ public class SettingsWalletForm extends WalletForm {
|
||||
EventManager.get().post(new WalletWatchLastChangedEvent(wallet, pastWallet, getWalletId(), walletCopy.getWatchLast()));
|
||||
}
|
||||
|
||||
List<Keystore> encryptionChangedKeystores = getEncryptionChangedKeystores(wallet, walletCopy);
|
||||
Wallet masterWallet = getMasterWallet();
|
||||
Wallet masterWalletCopy = walletCopy.isMasterWallet() ? walletCopy : walletCopy.getMasterWallet();
|
||||
List<Keystore> encryptionChangedKeystores = getEncryptionChangedKeystores(masterWallet, masterWalletCopy);
|
||||
if(!encryptionChangedKeystores.isEmpty()) {
|
||||
EventManager.get().post(new KeystoreEncryptionChangedEvent(wallet, pastWallet, getWalletId(), encryptionChangedKeystores));
|
||||
EventManager.get().post(new KeystoreEncryptionChangedEvent(masterWallet, masterWallet.copy(), getStorage().getWalletId(masterWallet), encryptionChangedKeystores));
|
||||
}
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
Wallet childWalletCopy = walletCopy.getChildWallet(childWallet.getName());
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
Wallet childWalletCopy = masterWalletCopy.getChildWallet(childWallet.getName());
|
||||
if(childWalletCopy != null) {
|
||||
Wallet pastChildWallet = childWallet.copy();
|
||||
List<Keystore> childEncryptionChangedKeystores = getEncryptionChangedKeystores(childWallet, childWalletCopy);
|
||||
if(!childEncryptionChangedKeystores.isEmpty()) {
|
||||
EventManager.get().post(new KeystoreEncryptionChangedEvent(childWallet, pastChildWallet, getStorage().getWalletId(childWallet), childEncryptionChangedKeystores));
|
||||
EventManager.get().post(new KeystoreEncryptionChangedEvent(childWallet, childWallet.copy(), getStorage().getWalletId(childWallet), childEncryptionChangedKeystores));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.WalletTabData;
|
||||
import com.sparrowwallet.sparrow.event.WalletBlockHeightChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletEntryLabelsChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletTabsClosedEvent;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.IntegerPropertyBase;
|
||||
import javafx.beans.property.LongProperty;
|
||||
@@ -181,9 +182,12 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
|
||||
|
||||
@Override
|
||||
public int compareTo(TransactionEntry other) {
|
||||
int blockOrder = blockTransaction.compareBlockOrder(other.blockTransaction);
|
||||
if(blockOrder != 0) {
|
||||
return blockOrder;
|
||||
//This comparison must be identical to that of WalletTransactionsEntry.WalletTransaction
|
||||
if(blockTransaction.getHeight() != other.blockTransaction.getHeight()) {
|
||||
int blockOrder = blockTransaction.getComparisonHeight() - other.blockTransaction.getComparisonHeight();
|
||||
if(blockOrder != 0) {
|
||||
return blockOrder;
|
||||
}
|
||||
}
|
||||
|
||||
int valueOrder = Long.compare(other.getValue(), getValue());
|
||||
@@ -191,7 +195,7 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
|
||||
return valueOrder;
|
||||
}
|
||||
|
||||
return blockTransaction.compareTo(other.blockTransaction);
|
||||
return blockTransaction.getHash().compareTo(other.blockTransaction.getHash());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,6 +248,16 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
|
||||
return balance;
|
||||
}
|
||||
|
||||
public Long getVSizeFromTip() {
|
||||
if(!AppServices.getMempoolHistogram().isEmpty()) {
|
||||
Set<MempoolRateSize> rateSizes = AppServices.getMempoolHistogram().get(AppServices.getMempoolHistogram().lastKey());
|
||||
double feeRate = blockTransaction.getFeeRate();
|
||||
return rateSizes.stream().filter(rateSize -> rateSize.getFee() > feeRate).mapToLong(MempoolRateSize::getVSize).sum();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void blockHeightChanged(WalletBlockHeightChangedEvent event) {
|
||||
if(event.getWallet().equals(getWallet())) {
|
||||
|
||||
@@ -69,7 +69,13 @@ public class UtxoEntry extends HashIndexEntry {
|
||||
private ObjectProperty<AddressStatus> addressStatusProperty;
|
||||
|
||||
public final void setDuplicateAddress(boolean value) {
|
||||
addressStatusProperty().set(new AddressStatus(value));
|
||||
AddressStatus addressStatus = addressStatusProperty().get();
|
||||
addressStatusProperty().set(new AddressStatus(value, addressStatus.dustAttack));
|
||||
}
|
||||
|
||||
public final void setDustAttack(boolean value) {
|
||||
AddressStatus addressStatus = addressStatusProperty().get();
|
||||
addressStatusProperty().set(new AddressStatus(addressStatus.duplicate, value));
|
||||
}
|
||||
|
||||
public final boolean isDuplicateAddress() {
|
||||
@@ -78,7 +84,7 @@ public class UtxoEntry extends HashIndexEntry {
|
||||
|
||||
public final ObjectProperty<AddressStatus> addressStatusProperty() {
|
||||
if(addressStatusProperty == null) {
|
||||
addressStatusProperty = new SimpleObjectProperty<>(UtxoEntry.this, "addressStatus", new AddressStatus(false));
|
||||
addressStatusProperty = new SimpleObjectProperty<>(UtxoEntry.this, "addressStatus", new AddressStatus(false, false));
|
||||
}
|
||||
|
||||
return addressStatusProperty;
|
||||
@@ -86,9 +92,11 @@ public class UtxoEntry extends HashIndexEntry {
|
||||
|
||||
public class AddressStatus {
|
||||
private final boolean duplicate;
|
||||
private final boolean dustAttack;
|
||||
|
||||
public AddressStatus(boolean duplicate) {
|
||||
public AddressStatus(boolean duplicate, boolean dustAttack) {
|
||||
this.duplicate = duplicate;
|
||||
this.dustAttack = dustAttack;
|
||||
}
|
||||
|
||||
public UtxoEntry getUtxoEntry() {
|
||||
@@ -102,6 +110,10 @@ public class UtxoEntry extends HashIndexEntry {
|
||||
public boolean isDuplicate() {
|
||||
return duplicate;
|
||||
}
|
||||
|
||||
public boolean isDustAttack() {
|
||||
return dustAttack;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user