mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1140a678ad | ||
|
|
6e8d44bc8c | ||
|
|
ad3b384feb | ||
|
|
f38350b38d | ||
|
|
62060c9839 | ||
|
|
8975f6f666 | ||
|
|
c7351cd191 | ||
|
|
62b1dc3900 | ||
|
|
f37ff47850 | ||
|
|
cfaa1f6c6e | ||
|
|
91c94b94eb | ||
|
|
a5eb7da067 | ||
|
|
195dbcef3b | ||
|
|
24955604e2 | ||
|
|
0305afbc02 | ||
|
|
d4c3c3afa8 | ||
|
|
cda7835672 | ||
|
|
b4b679dd16 | ||
|
|
3efaec2859 | ||
|
|
a53812c12f | ||
|
|
686c008e97 | ||
|
|
4d60a20336 | ||
|
|
9879889875 | ||
|
|
4aee89a35b | ||
|
|
fd9648efd1 | ||
|
|
8f438cd0bc | ||
|
|
8b47701dbe | ||
|
|
ff571c3df4 | ||
|
|
201c9ccca3 | ||
|
|
cbae280cc8 | ||
|
|
1f44229e62 | ||
|
|
541c71a002 | ||
|
|
bb08b5599c | ||
|
|
fd70f03259 | ||
|
|
fbca6c691d | ||
|
|
e8cd56388f | ||
|
|
3dfd8210a8 | ||
|
|
f9199b65f0 | ||
|
|
6a001bd67f | ||
|
|
ee2f387cd5 | ||
|
|
95200c7143 | ||
|
|
d7511c62bf | ||
|
|
7a5f4ff294 | ||
|
|
2b145cb9cc | ||
|
|
13bd05853c | ||
|
|
59f3338842 | ||
|
|
2cc38dc8b0 | ||
|
|
0e9d97c221 | ||
|
|
fb0fd013d9 | ||
|
|
e7510d2275 | ||
|
|
e92d0f9b58 | ||
|
|
ea23bb51d9 | ||
|
|
2d3bf0b2fe | ||
|
|
617ad380c0 | ||
|
|
29ac15846d | ||
|
|
f4acd3e587 | ||
|
|
f057b92729 | ||
|
|
4bf02f833c | ||
|
|
7ef51e6a5d | ||
|
|
fdbcea1625 | ||
|
|
218c2720e0 | ||
|
|
91ad82a21c | ||
|
|
f4b3b3d55a | ||
|
|
db1b55cfa0 | ||
|
|
bd0aca66b5 | ||
|
|
22ad1cc5d1 | ||
|
|
d07a5f0a01 | ||
|
|
947013e088 | ||
|
|
25f441a6a8 | ||
|
|
ee5015f0d5 | ||
|
|
4f00fabd23 | ||
|
|
6927423d68 | ||
|
|
fffa636956 | ||
|
|
a02ac3dcd2 | ||
|
|
e56e3d9394 | ||
|
|
119d00233d | ||
|
|
da427610d6 | ||
|
|
46034b8f11 | ||
|
|
d49d5967b2 | ||
|
|
484ef5f399 | ||
|
|
740c00d1ba | ||
|
|
dfae39255e | ||
|
|
c2bce893db | ||
|
|
ef063fde75 | ||
|
|
adb446de3e | ||
|
|
d040f186a2 | ||
|
|
b4f9c52413 | ||
|
|
7527dd0909 | ||
|
|
b0be8ca7c2 | ||
|
|
1e0c0c1c75 | ||
|
|
d731f7296b | ||
|
|
12034a07d7 | ||
|
|
60e3d4e107 | ||
|
|
ad8e17a3a0 | ||
|
|
3e676eadcb | ||
|
|
3640db3d3d | ||
|
|
d0bf55de70 | ||
|
|
ad0b6adfd8 | ||
|
|
92b32b0d99 | ||
|
|
233addc1b7 | ||
|
|
1d8c37066e | ||
|
|
c450efe499 | ||
|
|
34bcc87468 | ||
|
|
2aac365039 | ||
|
|
7e68ecffd3 | ||
|
|
bf457620db | ||
|
|
e50fe4c68c | ||
|
|
1bbc586cd6 | ||
|
|
e1dab3a48e | ||
|
|
73b672a7ef | ||
|
|
b142c54c68 | ||
|
|
58d09c3ba7 | ||
|
|
d5a7a5b855 | ||
|
|
fcb83f8bfa | ||
|
|
f187603ec3 | ||
|
|
8d7308bc37 | ||
|
|
e44d1393f5 | ||
|
|
33ba472843 | ||
|
|
faa81f2273 | ||
|
|
0646c8aa28 | ||
|
|
deb47ca002 | ||
|
|
ec131bb8da | ||
|
|
31f287125f | ||
|
|
e131f645f6 | ||
|
|
eabc0da6d5 | ||
|
|
49573d1075 | ||
|
|
17093dbf72 | ||
|
|
c2b5b24702 | ||
|
|
65f1fa7cf8 | ||
|
|
cbee341544 | ||
|
|
95b1aa8e48 | ||
|
|
af89be96e5 | ||
|
|
fad960c192 | ||
|
|
1adeef04db | ||
|
|
47f925b677 | ||
|
|
5db3096386 | ||
|
|
62e98b0090 | ||
|
|
76490604ac | ||
|
|
783733b9d3 | ||
|
|
041b5aa34c | ||
|
|
33d23e9ea5 | ||
|
|
b3f6cc88f0 | ||
|
|
b912aa2eb9 | ||
|
|
d894343457 | ||
|
|
fb1e1cefda | ||
|
|
d960bdce96 | ||
|
|
fb679c0199 | ||
|
|
9ecfe0e94f | ||
|
|
1bc2f7d69f | ||
|
|
6e4d308c79 | ||
|
|
afb6037449 | ||
|
|
369983748d | ||
|
|
0d16c87b40 | ||
|
|
b59a65dcfe | ||
|
|
87cc28e0a4 | ||
|
|
1187925543 | ||
|
|
cd4edab4ae | ||
|
|
daf320f36b | ||
|
|
f6ff92865b | ||
|
|
d420c71673 | ||
|
|
07101b3ca0 | ||
|
|
00f7f3f5b3 | ||
|
|
5d2c133401 | ||
|
|
7b0dfd66a7 | ||
|
|
83719e7df2 | ||
|
|
f1b246f0b0 | ||
|
|
599880ea5c | ||
|
|
d625bab02e | ||
|
|
1676676e06 | ||
|
|
f7e603118f | ||
|
|
f6fd889712 | ||
|
|
21d91d3d10 | ||
|
|
f1cddc28e7 | ||
|
|
1887e1c7b0 | ||
|
|
3e870f362d | ||
|
|
665d70b845 | ||
|
|
c2cbe62a5a | ||
|
|
c6b6e74515 | ||
|
|
8ddc494b53 | ||
|
|
33f439f49a | ||
|
|
d68ab40c94 | ||
|
|
31346e2afa | ||
|
|
c407a41475 | ||
|
|
8baa8e2e96 | ||
|
|
72adbe44a7 | ||
|
|
dd64c18c21 | ||
|
|
2354b061a9 | ||
|
|
a5050117a3 | ||
|
|
f245b57022 | ||
|
|
d3752a856b | ||
|
|
fe7dba6d83 | ||
|
|
2d0a94d024 | ||
|
|
41146310d6 | ||
|
|
a167f6aedb | ||
|
|
0fed7c45ee | ||
|
|
5a0df265bc | ||
|
|
646b8b0e65 | ||
|
|
c9b40b1973 | ||
|
|
9ec5b6ce26 | ||
|
|
93893111c6 | ||
|
|
3600d32ffd |
@@ -10,16 +10,16 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-2022, ubuntu-20.04, macos-12]
|
||||
os: [windows-2022, ubuntu-20.04, ubuntu-22.04-arm, macos-13, macos-14]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 18.0.1
|
||||
- name: Set up JDK 22.0.2
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '18.0.1'
|
||||
java-version: '22.0.2'
|
||||
- name: Show Build Versions
|
||||
run: ./gradlew -v
|
||||
- name: Build with Gradle
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }}
|
||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
|
||||
path: |
|
||||
build/jpackage/*
|
||||
!build/jpackage/Sparrow/
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }} Headless
|
||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }} Headless
|
||||
path: |
|
||||
build/jpackage/*
|
||||
!build/jpackage/Sparrow/
|
||||
@@ -1,3 +1,6 @@
|
||||
[submodule "drongo"]
|
||||
path = drongo
|
||||
url = ../../sparrowwallet/drongo.git
|
||||
[submodule "lark"]
|
||||
path = lark
|
||||
url = ../../sparrowwallet/lark.git
|
||||
|
||||
@@ -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 18 or higher to be installed.
|
||||
The release binaries are built with [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
|
||||
In order to build, Sparrow requires Java 22 or higher to be installed.
|
||||
The release binaries are built with [Eclipse Temurin 22.0.2+9](https://github.com/adoptium/temurin22-binaries/releases/tag/jdk-22.0.2%2B9).
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
|
||||
@@ -44,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
|
||||
|
||||
`./sparrow`
|
||||
|
||||
Java 18 or higher must be installed.
|
||||
Java 22 or higher must be installed.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -64,10 +64,12 @@ Usage: sparrow [options]
|
||||
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
|
||||
--network, -n
|
||||
Network to use
|
||||
Possible Values: [mainnet, testnet, regtest, signet]
|
||||
Possible Values: [mainnet, testnet, regtest, signet, testnet4]
|
||||
```
|
||||
|
||||
As a fallback, the network (mainnet, testnet, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
Note that testnet currently refers to testnet3.
|
||||
|
||||
As a fallback, the network (mainnet, testnet, testnet4, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
|
||||
`export SPARROW_NETWORK=testnet`
|
||||
|
||||
@@ -83,7 +85,7 @@ When not explicitly configured using the command line argument above, Sparrow st
|
||||
| Linux | ~/.sparrow |
|
||||
| Windows | %APPDATA%/Sparrow |
|
||||
|
||||
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
Testnet3, testnet4, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
|
||||
+126
-169
@@ -1,13 +1,11 @@
|
||||
import java.awt.GraphicsEnvironment
|
||||
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'extra-java-module-info'
|
||||
id 'org-openjfx-javafxplugin'
|
||||
id 'org.beryx.jlink' version '3.0.1'
|
||||
id 'org.beryx.jlink' version '3.1.1'
|
||||
id 'org.gradlex.extra-java-module-info' version '1.9'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.8.5'
|
||||
def sparrowVersion = '2.1.2'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -41,10 +39,7 @@ version "${sparrowVersion}"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://oss.sonatype.org/content/groups/public' }
|
||||
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url 'https://maven.ecs.soton.ac.uk/content/groups/maven.openimaj.org/' }
|
||||
maven { url 'https://code.sparrowwallet.com/api/packages/sparrowwallet/maven' }
|
||||
}
|
||||
|
||||
tasks.withType(AbstractArchiveTask) {
|
||||
@@ -53,7 +48,7 @@ tasks.withType(AbstractArchiveTask) {
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = "18"
|
||||
version = headless ? "18" : "23.0.2"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
@@ -64,6 +59,7 @@ java {
|
||||
dependencies {
|
||||
//Any changes to the dependencies must be reflected in the module definitions below!
|
||||
implementation(project(':drongo'))
|
||||
implementation(project(':lark'))
|
||||
implementation('com.google.guava:guava:33.0.0-jre')
|
||||
implementation('com.google.code.gson:gson:2.9.1')
|
||||
implementation('com.h2database:h2:2.1.214')
|
||||
@@ -76,13 +72,13 @@ dependencies {
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.flywaydb:flyway-core:7.10.7-SNAPSHOT')
|
||||
implementation('org.flywaydb:flyway-core:9.22.3')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||
implementation('com.google.zxing:javase:3.4.0') {
|
||||
exclude group: 'com.beust', module: 'jcommander'
|
||||
}
|
||||
implementation('com.beust:jcommander:1.81')
|
||||
implementation('org.jcommander:jcommander:2.0')
|
||||
implementation('com.github.arteam:simple-json-rpc-core:1.3')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.3') {
|
||||
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
||||
@@ -90,7 +86,8 @@ dependencies {
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.7.3')
|
||||
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
|
||||
implementation('com.sparrowwallet:hummingbird:1.7.4')
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
@@ -98,15 +95,27 @@ dependencies {
|
||||
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
|
||||
exclude group: 'com.nativelibs4java', module: 'bridj'
|
||||
}
|
||||
implementation "io.matthewnelson.kotlin-components:kmp-tor:${vTor}-${vKmpTor}"
|
||||
if(kmpOs == "linux" && kmpArch == "arm64") {
|
||||
implementation("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm:${vTor}")
|
||||
} else {
|
||||
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}:${vTor}")
|
||||
implementation("io.matthewnelson.kotlin-components:kmp-tor:${vTor}-${vKmpTor}") {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
}
|
||||
if(kmpOs == "linux" && kmpArch == "arm64") {
|
||||
implementation("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm:${vTor}") {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
}
|
||||
} else {
|
||||
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}:${vTor}") {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
}
|
||||
}
|
||||
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-extract:${vTor}") {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
}
|
||||
implementation("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager:${vKmpTor}") {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
}
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.7.1') {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
}
|
||||
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-extract:${vTor}")
|
||||
implementation("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager:${vKmpTor}")
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.7.1')
|
||||
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
@@ -124,17 +133,16 @@ dependencies {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
||||
implementation('io.samourai.code.whirlpool:whirlpool-client:1.0.6')
|
||||
implementation('io.samourai.code.wallet:java-http-client:2.0.2')
|
||||
implementation('com.sparrowwallet:tern:1.0.6')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
implementation('org.apache.commons:commons-compress:1.25.0')
|
||||
implementation('org.apache.commons:commons-compress:1.27.1')
|
||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.30')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.1')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.3')
|
||||
implementation('net.coobird:thumbnailator:0.4.18')
|
||||
implementation('com.github.hervegirod:fxsvgimage:1.0b2')
|
||||
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||
implementation('com.jcraft:jzlib:1.1.3')
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
||||
@@ -161,7 +169,7 @@ processResources {
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson"]
|
||||
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson", "--add-reads=org.flywaydb.core=java.desktop"]
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -189,7 +197,8 @@ application {
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
|
||||
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||
|
||||
if(os.macOsX) {
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
||||
@@ -210,12 +219,10 @@ jlink {
|
||||
requires 'jdk.crypto.cryptoki'
|
||||
requires 'java.management'
|
||||
requires 'io.leangen.geantyref'
|
||||
uses 'org.flywaydb.core.extensibility.FlywayExtension'
|
||||
uses 'org.flywaydb.core.internal.database.DatabaseType'
|
||||
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
|
||||
}
|
||||
|
||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
launcher {
|
||||
name = 'sparrow'
|
||||
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
@@ -249,7 +256,8 @@ jlink {
|
||||
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor",
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg",
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
|
||||
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||
|
||||
if(os.windows) {
|
||||
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
|
||||
@@ -272,11 +280,11 @@ jlink {
|
||||
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']
|
||||
installerType = "exe"
|
||||
installerType = "msi"
|
||||
}
|
||||
if(os.linux) {
|
||||
if(headless) {
|
||||
installerOptions = ['--license-file', 'LICENSE']
|
||||
installerOptions = ['--license-file', 'LICENSE', '--resource-dir', "src/main/deploy/package/linux-headless/${osArch}"]
|
||||
} else {
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||
}
|
||||
@@ -289,22 +297,48 @@ jlink {
|
||||
installerType = "dmg"
|
||||
}
|
||||
}
|
||||
if(os.linux) {
|
||||
jpackageImage {
|
||||
dependsOn('prepareModulesDir', 'copyUdevRules')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task removeGroupWritePermission(type: Exec) {
|
||||
if(os.linux) {
|
||||
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules')
|
||||
} else {
|
||||
tasks.jlink.finalizedBy('addUserWritePermission')
|
||||
}
|
||||
|
||||
tasks.register('addUserWritePermission', Exec) {
|
||||
if(os.windows) {
|
||||
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T'
|
||||
} else {
|
||||
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('copyUdevRules', Copy) {
|
||||
from('lark/src/main/resources/udev')
|
||||
into(layout.buildDirectory.dir('image/conf/udev'))
|
||||
include('*')
|
||||
}
|
||||
|
||||
tasks.register('removeGroupWritePermission', Exec) {
|
||||
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
||||
}
|
||||
|
||||
task packageZipDistribution(type: Zip) {
|
||||
tasks.register('packageZipDistribution', Zip) {
|
||||
archiveFileName = "Sparrow-${sparrowVersion}.zip"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
preserveFileTimestamps = os.macOsX
|
||||
from("$buildDir/jpackage/") {
|
||||
include "Sparrow/**"
|
||||
include "Sparrow.app/**"
|
||||
}
|
||||
}
|
||||
|
||||
task packageTarDistribution(type: Tar) {
|
||||
tasks.register('packageTarDistribution', Tar) {
|
||||
dependsOn removeGroupWritePermission
|
||||
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
@@ -315,60 +349,11 @@ task packageTarDistribution(type: Tar) {
|
||||
}
|
||||
|
||||
extraJavaModuleInfo {
|
||||
module('jackson-core-2.13.2.jar', 'com.fasterxml.jackson.core', '2.13.2') {
|
||||
exports('com.fasterxml.jackson.core')
|
||||
exports('com.fasterxml.jackson.core.async')
|
||||
exports('com.fasterxml.jackson.core.base')
|
||||
exports('com.fasterxml.jackson.core.exc')
|
||||
exports('com.fasterxml.jackson.core.filter')
|
||||
exports('com.fasterxml.jackson.core.format')
|
||||
exports('com.fasterxml.jackson.core.io')
|
||||
exports('com.fasterxml.jackson.core.json')
|
||||
exports('com.fasterxml.jackson.core.json.async')
|
||||
exports('com.fasterxml.jackson.core.sym')
|
||||
exports('com.fasterxml.jackson.core.type')
|
||||
exports('com.fasterxml.jackson.core.util')
|
||||
uses('com.fasterxml.jackson.core.ObjectCodec')
|
||||
}
|
||||
module('jackson-annotations-2.13.2.jar', 'com.fasterxml.jackson.annotation', '2.13.2') {
|
||||
requires('com.fasterxml.jackson.core')
|
||||
exports('com.fasterxml.jackson.annotation')
|
||||
}
|
||||
module('jackson-databind-2.13.2.jar', 'com.fasterxml.jackson.databind', '2.13.2') {
|
||||
requires('java.desktop')
|
||||
requires('java.logging')
|
||||
requires('com.fasterxml.jackson.annotation')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
requires('java.sql')
|
||||
requires('java.xml')
|
||||
exports('com.fasterxml.jackson.databind')
|
||||
exports('com.fasterxml.jackson.databind.annotation')
|
||||
exports('com.fasterxml.jackson.databind.cfg')
|
||||
exports('com.fasterxml.jackson.databind.deser')
|
||||
exports('com.fasterxml.jackson.databind.deser.impl')
|
||||
exports('com.fasterxml.jackson.databind.deser.std')
|
||||
exports('com.fasterxml.jackson.databind.exc')
|
||||
exports('com.fasterxml.jackson.databind.ext')
|
||||
exports('com.fasterxml.jackson.databind.introspect')
|
||||
exports('com.fasterxml.jackson.databind.json')
|
||||
exports('com.fasterxml.jackson.databind.jsonFormatVisitors')
|
||||
exports('com.fasterxml.jackson.databind.jsonschema')
|
||||
exports('com.fasterxml.jackson.databind.jsontype')
|
||||
exports('com.fasterxml.jackson.databind.jsontype.impl')
|
||||
exports('com.fasterxml.jackson.databind.module')
|
||||
exports('com.fasterxml.jackson.databind.node')
|
||||
exports('com.fasterxml.jackson.databind.ser')
|
||||
exports('com.fasterxml.jackson.databind.ser.impl')
|
||||
exports('com.fasterxml.jackson.databind.ser.std')
|
||||
exports('com.fasterxml.jackson.databind.type')
|
||||
exports('com.fasterxml.jackson.databind.util')
|
||||
uses('com.fasterxml.jackson.databind.Module')
|
||||
}
|
||||
module('tornadofx-controls-1.0.4.jar', 'tornadofx.controls', '1.0.4') {
|
||||
module('no.tornado:tornadofx-controls', 'tornadofx.controls') {
|
||||
exports('tornadofx.control')
|
||||
requires('javafx.controls')
|
||||
}
|
||||
module('simple-json-rpc-core-1.3.jar', 'simple.json.rpc.core', '1.3') {
|
||||
module('com.github.arteam:simple-json-rpc-core', 'simple.json.rpc.core') {
|
||||
exports('com.github.arteam.simplejsonrpc.core.annotation')
|
||||
exports('com.github.arteam.simplejsonrpc.core.domain')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
@@ -376,7 +361,7 @@ extraJavaModuleInfo {
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('org.jetbrains.annotations')
|
||||
}
|
||||
module('simple-json-rpc-client-1.3.jar', 'simple.json.rpc.client', '1.3') {
|
||||
module('com.github.arteam:simple-json-rpc-client', 'simple.json.rpc.client') {
|
||||
exports('com.github.arteam.simplejsonrpc.client')
|
||||
exports('com.github.arteam.simplejsonrpc.client.builder')
|
||||
exports('com.github.arteam.simplejsonrpc.client.exception')
|
||||
@@ -384,19 +369,19 @@ extraJavaModuleInfo {
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('simple.json.rpc.core')
|
||||
}
|
||||
module('simple-json-rpc-server-1.3.jar', 'simple.json.rpc.server', '1.3') {
|
||||
module('com.github.arteam:simple-json-rpc-server', 'simple.json.rpc.server') {
|
||||
exports('com.github.arteam.simplejsonrpc.server')
|
||||
requires('simple.json.rpc.core')
|
||||
requires('com.google.common')
|
||||
requires('org.slf4j')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
}
|
||||
module("bridj${targetName}-0.7-20140918-3.jar", 'com.nativelibs4java.bridj', '0.7-20140918-3') {
|
||||
module("com.nativelibs4java:bridj${targetName}", 'com.nativelibs4java.bridj') {
|
||||
exports('org.bridj')
|
||||
exports('org.bridj.cpp')
|
||||
requires('java.logging')
|
||||
}
|
||||
module("webcam-capture${targetName}-0.3.13-SNAPSHOT.jar", 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
|
||||
module("com.github.sarxos:webcam-capture${targetName}", 'com.github.sarxos.webcam.capture') {
|
||||
exports('com.github.sarxos.webcam')
|
||||
exports('com.github.sarxos.webcam.ds.buildin')
|
||||
exports('com.github.sarxos.webcam.ds.buildin.natives')
|
||||
@@ -404,26 +389,19 @@ extraJavaModuleInfo {
|
||||
requires('com.nativelibs4java.bridj')
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('centerdevice-nsmenufx-2.1.7.jar', 'centerdevice.nsmenufx', '2.1.7') {
|
||||
module('de.codecentric.centerdevice:centerdevice-nsmenufx', 'centerdevice.nsmenufx') {
|
||||
exports('de.codecentric.centerdevice')
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
|
||||
module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
|
||||
exports('com.csvreader')
|
||||
}
|
||||
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
|
||||
exports('org.zeromq')
|
||||
}
|
||||
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
|
||||
exports('org.json.simple')
|
||||
exports('org.json.simple.parser')
|
||||
}
|
||||
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
|
||||
module('jsr305-3.0.2.jar', 'com.google.code.findbugs.jsr305', '3.0.2')
|
||||
module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture')
|
||||
module('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305')
|
||||
module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8')
|
||||
module('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
|
||||
module('org.jdbi:jdbi3-core', 'org.jdbi.v3.core') {
|
||||
exports('org.jdbi.v3.core')
|
||||
exports('org.jdbi.v3.core.mapper')
|
||||
exports('org.jdbi.v3.core.statement')
|
||||
@@ -435,10 +413,10 @@ extraJavaModuleInfo {
|
||||
requires('org.slf4j')
|
||||
requires('com.github.benmanes.caffeine')
|
||||
}
|
||||
module('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
|
||||
module('io.leangen.geantyref:geantyref', 'io.leangen.geantyref') {
|
||||
exports('io.leangen.geantyref')
|
||||
}
|
||||
module('richtextfx-0.10.4.jar', 'org.fxmisc.richtext', '0.10.4') {
|
||||
module('org.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') {
|
||||
exports('org.fxmisc.richtext')
|
||||
exports('org.fxmisc.richtext.event')
|
||||
exports('org.fxmisc.richtext.model')
|
||||
@@ -450,20 +428,20 @@ extraJavaModuleInfo {
|
||||
requires('org.fxmisc.undo.undofx')
|
||||
requires('org.fxmisc.wellbehaved')
|
||||
}
|
||||
module('undofx-2.1.0.jar', 'org.fxmisc.undo.undofx', '2.1.0') {
|
||||
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo.undofx') {
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
requires('org.reactfx.reactfx')
|
||||
}
|
||||
module('flowless-0.6.1.jar', 'org.fxmisc.flowless', '0.6.1') {
|
||||
module('org.fxmisc.flowless:flowless', 'org.fxmisc.flowless') {
|
||||
exports('org.fxmisc.flowless')
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
requires('org.reactfx.reactfx')
|
||||
}
|
||||
module('reactfx-2.0-M5.jar', 'org.reactfx.reactfx', '2.0-M5') {
|
||||
module('org.reactfx:reactfx', 'org.reactfx.reactfx') {
|
||||
exports('org.reactfx')
|
||||
exports('org.reactfx.value')
|
||||
exports('org.reactfx.collection')
|
||||
@@ -472,70 +450,42 @@ extraJavaModuleInfo {
|
||||
requires('javafx.graphics')
|
||||
requires('javafx.controls')
|
||||
}
|
||||
module('rxjavafx-2.2.2.jar', 'io.reactivex.rxjava2fx', '2.2.2') {
|
||||
module('io.reactivex.rxjava2:rxjavafx', 'io.reactivex.rxjava2fx') {
|
||||
exports('io.reactivex.rxjavafx.schedulers')
|
||||
requires('io.reactivex.rxjava2')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
|
||||
module('org.flywaydb:flyway-core', 'org.flywaydb.core') {
|
||||
exports('org.flywaydb.core')
|
||||
exports('org.flywaydb.core.api')
|
||||
exports('org.flywaydb.core.api.exception')
|
||||
exports('org.flywaydb.core.api.configuration')
|
||||
uses('org.flywaydb.core.extensibility.Plugin')
|
||||
requires('java.sql')
|
||||
}
|
||||
module('org.fxmisc.wellbehaved:wellbehavedfx', 'org.fxmisc.wellbehaved') {
|
||||
requires('javafx.base')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
|
||||
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
|
||||
exports('com.sparrowwallet.hummingbird')
|
||||
exports('com.sparrowwallet.hummingbird.registry')
|
||||
requires('co.nstant.in.cbor')
|
||||
module('com.github.jai-imageio:jai-imageio-core', 'com.github.jai.imageio.jai.imageio.core') {
|
||||
requires('java.desktop')
|
||||
}
|
||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||
module('co.nstant.in:cbor', 'co.nstant.in.cbor') {
|
||||
exports('co.nstant.in.cbor')
|
||||
exports('co.nstant.in.cbor.model')
|
||||
exports('co.nstant.in.cbor.builder')
|
||||
}
|
||||
module('commons-codec-1.10.jar', 'commons.codec', '1.10') {
|
||||
exports('org.apache.commons.codec')
|
||||
}
|
||||
module('logback-core-1.2.13.jar', 'ch.qos.logback.core', '1.2.13') {
|
||||
exports('ch.qos.logback.core')
|
||||
}
|
||||
module('jackson-datatype-jsr310-2.13.2.jar', 'jackson-datatype-jsr310', '2.13.2') {
|
||||
exports('com.fasterxml.jackson.datatype.jsr310')
|
||||
}
|
||||
module('json-20240205.jar', 'org.json', '20240205') {
|
||||
exports('org.json')
|
||||
}
|
||||
module('scrypt-1.4.0.jar', 'scrypt', '1.4.0') {
|
||||
exports('com.lambdaworks.codec')
|
||||
exports('com.lambdaworks.crypto')
|
||||
}
|
||||
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
|
||||
exports('okio')
|
||||
}
|
||||
module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') {
|
||||
exports('com.auth0.jwt')
|
||||
}
|
||||
module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
|
||||
module('net.sourceforge.streamsupport:streamsupport', 'net.sourceforge.streamsupport') {
|
||||
requires('jdk.unsupported')
|
||||
exports('java8.util')
|
||||
exports('java8.util.function')
|
||||
exports('java8.util.stream')
|
||||
}
|
||||
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
|
||||
exports('org.apache.commons.text')
|
||||
}
|
||||
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
|
||||
exports('net.jcip.annotations')
|
||||
}
|
||||
module('thumbnailator-0.4.18.jar', 'net.coobird.thumbnailator', '0.4.18') {
|
||||
module('net.coobird:thumbnailator', 'net.coobird.thumbnailator') {
|
||||
exports('net.coobird.thumbnailator')
|
||||
requires('java.desktop')
|
||||
}
|
||||
module('fxsvgimage-1.0b2.jar', 'com.github.hervegirod', '1.0b2') {
|
||||
exports('org.girod.javafx.svgimage')
|
||||
requires('javafx.graphics')
|
||||
requires('java.xml')
|
||||
}
|
||||
module("kmp-tor-jvm-${vKmpTor}.jar", 'kmp.tor.jvm', "${vTor}-${vKmpTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-jvm", 'kmp.tor.jvm') {
|
||||
exports('io.matthewnelson.kmp.tor')
|
||||
requires('kmp.tor.binary.extract.jvm')
|
||||
requires('kmp.tor.manager.jvm')
|
||||
@@ -546,24 +496,24 @@ extraJavaModuleInfo {
|
||||
requires('java.management')
|
||||
}
|
||||
if(kmpOs == "linux" && kmpArch == "arm64") {
|
||||
module("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") {
|
||||
module("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm", "kmp.tor.binary.${kmpOs}${kmpArch}") {
|
||||
exports("io.matthewnelson.kmp.tor.resource.${kmpOs}.${kmpArch}")
|
||||
exports("kmptor.${kmpOs}.${kmpArch}")
|
||||
}
|
||||
} else {
|
||||
module("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}-jvm", "kmp.tor.binary.${kmpOs}${kmpArch}") {
|
||||
exports("io.matthewnelson.kmp.tor.binary.${kmpOs}.${kmpArch}")
|
||||
exports("kmptor.${kmpOs}.${kmpArch}")
|
||||
}
|
||||
}
|
||||
module("kmp-tor-binary-extract-jvm-${vTor}.jar", 'kmp.tor.binary.extract.jvm', "${vTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-binary-extract-jvm", 'kmp.tor.binary.extract.jvm') {
|
||||
exports('io.matthewnelson.kmp.tor.binary.extract')
|
||||
exports('io.matthewnelson.kmp.tor.binary.extract.internal')
|
||||
requires('kotlin.stdlib')
|
||||
requires("kmp.tor.binary.${kmpOs}${kmpArch}")
|
||||
requires('kmp.tor.binary.geoip.jvm')
|
||||
}
|
||||
module("kmp-tor-manager-jvm-${vKmpTor}.jar", 'kmp.tor.manager.jvm', "${vKmpTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-manager-jvm", 'kmp.tor.manager.jvm') {
|
||||
exports('io.matthewnelson.kmp.tor.manager')
|
||||
exports('io.matthewnelson.kmp.tor.manager.util')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
@@ -574,7 +524,7 @@ extraJavaModuleInfo {
|
||||
requires('kmp.tor.controller.jvm')
|
||||
requires('kmp.tor.common.jvm')
|
||||
}
|
||||
module("kmp-tor-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.manager.common.jvm', "${vKmpTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-manager-common-jvm", 'kmp.tor.manager.common.jvm') {
|
||||
exports('io.matthewnelson.kmp.tor.manager.common')
|
||||
exports('io.matthewnelson.kmp.tor.manager.common.event')
|
||||
exports('io.matthewnelson.kmp.tor.manager.common.state')
|
||||
@@ -582,7 +532,7 @@ extraJavaModuleInfo {
|
||||
requires('kmp.tor.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
}
|
||||
module("kmp-tor-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.controller.common.jvm', "${vKmpTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-controller-common-jvm", 'kmp.tor.controller.common.jvm') {
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.config')
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.file')
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.control')
|
||||
@@ -593,12 +543,12 @@ extraJavaModuleInfo {
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.atomicfu')
|
||||
}
|
||||
module("kmp-tor-common-jvm-${vKmpTor}.jar", 'kmp.tor.common.jvm', "${vKmpTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-common-jvm", 'kmp.tor.common.jvm') {
|
||||
exports('io.matthewnelson.kmp.tor.common.address')
|
||||
requires('parcelize.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
}
|
||||
module("kmp-tor-controller-jvm-${vKmpTor}.jar", 'kmp.tor.controller.jvm', "${vKmpTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-controller-jvm", 'kmp.tor.controller.jvm') {
|
||||
exports('io.matthewnelson.kmp.tor.controller.internal.controller')
|
||||
requires('kmp.tor.common.jvm')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
@@ -608,10 +558,10 @@ extraJavaModuleInfo {
|
||||
requires('encoding.core.jvm')
|
||||
requires('encoding.base16.jvm')
|
||||
}
|
||||
module("kmp-tor-ext-callback-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.common.jvm', "${vKmpTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-common-jvm", 'kmp.tor.ext.callback.common.jvm') {
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.common')
|
||||
}
|
||||
module("kmp-tor-ext-callback-manager-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.jvm', "${vKmpTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager-jvm", 'kmp.tor.ext.callback.manager.jvm') {
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.manager')
|
||||
requires('kmp.tor.manager.jvm')
|
||||
requires('kmp.tor.ext.callback.common.jvm')
|
||||
@@ -622,15 +572,15 @@ extraJavaModuleInfo {
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.coroutines.core')
|
||||
}
|
||||
module("kmp-tor-ext-callback-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.common.jvm', "${vKmpTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager-common-jvm", 'kmp.tor.ext.callback.manager.common.jvm') {
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.manager.common')
|
||||
requires('kmp.tor.ext.callback.controller.common.jvm')
|
||||
}
|
||||
module("kmp-tor-ext-callback-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.controller.common.jvm', "${vKmpTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-controller-common-jvm", 'kmp.tor.ext.callback.controller.common.jvm') {
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control')
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control.usecase')
|
||||
}
|
||||
module("kmp-tor-binary-geoip-jvm-${vTor}.jar", 'kmp.tor.binary.geoip.jvm', "${vTor}") {
|
||||
module("io.matthewnelson.kotlin-components:kmp-tor-binary-geoip-jvm", 'kmp.tor.binary.geoip.jvm') {
|
||||
exports('io.matthewnelson.kmp.tor.binary.geoip')
|
||||
exports('kmptor')
|
||||
}
|
||||
@@ -648,11 +598,18 @@ extraJavaModuleInfo {
|
||||
module("parcelize-jvm-0.1.2.jar", 'parcelize.jvm', "0.1.2") {
|
||||
exports('io.matthewnelson.component.parcelize')
|
||||
}
|
||||
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
|
||||
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
|
||||
module('org.jcommander:jcommander', 'org.jcommander') {
|
||||
exports('com.beust.jcommander')
|
||||
}
|
||||
module('jzlib-1.1.3.jar', 'com.jcraft.jzlib', '1.1.3') {
|
||||
module('com.sparrowwallet:hid4java', 'org.hid4java') {
|
||||
requires('com.sun.jna')
|
||||
exports('org.hid4java')
|
||||
exports('org.hid4java.jna')
|
||||
}
|
||||
module('com.sparrowwallet:usb4java', 'org.usb4java') {
|
||||
exports('org.usb4java')
|
||||
}
|
||||
module('com.jcraft:jzlib', 'com.jcraft.jzlib') {
|
||||
exports('com.jcraft.jzlib')
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ plugins {
|
||||
dependencies {
|
||||
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
|
||||
implementation 'org.javamodularity:moduleplugin:1.8.14'
|
||||
implementation 'org.ow2.asm:asm:9.6'
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -17,11 +16,6 @@ repositories {
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
// here we register our plugin with an ID
|
||||
register("extra-java-module-info") {
|
||||
id = "extra-java-module-info"
|
||||
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
|
||||
}
|
||||
register("org-openjfx-javafxplugin") {
|
||||
id = "org-openjfx-javafxplugin"
|
||||
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
|
||||
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.attributes.Attribute;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
|
||||
/**
|
||||
* Entry point of our plugin that should be applied in the root project.
|
||||
*/
|
||||
public class ExtraModuleInfoPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
|
||||
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
|
||||
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
|
||||
|
||||
// setup the transform for all projects in the build
|
||||
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
|
||||
}
|
||||
|
||||
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
|
||||
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
|
||||
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
|
||||
|
||||
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
|
||||
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
|
||||
c -> c.getAttributes().attribute(javaModule, true));
|
||||
|
||||
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
|
||||
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
|
||||
|
||||
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
|
||||
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
|
||||
t.parameters(p -> {
|
||||
p.setModuleInfo(extension.getModuleInfo());
|
||||
p.setAutomaticModules(extension.getAutomaticModules());
|
||||
});
|
||||
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
|
||||
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
|
||||
if (!configuration.isCanBeResolved()) {
|
||||
return false;
|
||||
}
|
||||
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
||||
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
||||
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
|
||||
import org.gradle.api.Action;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A data class to collect all the module information we want to add.
|
||||
* Here the class is used as extension that can be configured in the build script
|
||||
* and as input to the ExtraModuleInfoTransform that add the information to Jars.
|
||||
*/
|
||||
public class ExtraModuleInfoPluginExtension {
|
||||
|
||||
private final Map<String, ModuleInfo> moduleInfo = new HashMap<>();
|
||||
private final Map<String, String> automaticModules = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Add full module information for a given Jar file.
|
||||
*/
|
||||
public void module(String jarName, String moduleName, String moduleVersion) {
|
||||
module(jarName, moduleName, moduleVersion, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add full module information, including exported packages and dependencies, for a given Jar file.
|
||||
*/
|
||||
public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action<? super ModuleInfo> conf) {
|
||||
ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion);
|
||||
if (conf != null) {
|
||||
conf.execute(moduleInfo);
|
||||
}
|
||||
this.moduleInfo.put(jarName, moduleInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add only an automatic module name to a given jar file.
|
||||
*/
|
||||
public void automaticModule(String jarName, String moduleName) {
|
||||
automaticModules.put(jarName, moduleName);
|
||||
}
|
||||
|
||||
protected Map<String, ModuleInfo> getModuleInfo() {
|
||||
return moduleInfo;
|
||||
}
|
||||
|
||||
protected Map<String, String> getAutomaticModules() {
|
||||
return automaticModules;
|
||||
}
|
||||
}
|
||||
-176
@@ -1,176 +0,0 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import org.gradle.api.artifacts.transform.InputArtifact;
|
||||
import org.gradle.api.artifacts.transform.TransformAction;
|
||||
import org.gradle.api.artifacts.transform.TransformOutputs;
|
||||
import org.gradle.api.artifacts.transform.TransformParameters;
|
||||
import org.gradle.api.file.FileSystemLocation;
|
||||
import org.gradle.api.provider.Provider;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.ModuleVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.jar.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
/**
|
||||
* An artifact transform that applies additional information to Jars without module information.
|
||||
* The transformation fails the build if a Jar does not contain information and no extra information
|
||||
* was defined for it. This way we make sure that all Jars are turned into modules.
|
||||
*/
|
||||
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
|
||||
|
||||
public static class Parameter implements TransformParameters, Serializable {
|
||||
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
|
||||
private Map<String, String> automaticModules = Collections.emptyMap();
|
||||
|
||||
@Input
|
||||
public Map<String, ModuleInfo> getModuleInfo() {
|
||||
return moduleInfo;
|
||||
}
|
||||
|
||||
@Input
|
||||
public Map<String, String> getAutomaticModules() {
|
||||
return automaticModules;
|
||||
}
|
||||
|
||||
public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
|
||||
this.moduleInfo = moduleInfo;
|
||||
}
|
||||
|
||||
public void setAutomaticModules(Map<String, String> automaticModules) {
|
||||
this.automaticModules = automaticModules;
|
||||
}
|
||||
}
|
||||
|
||||
@InputArtifact
|
||||
protected abstract Provider<FileSystemLocation> getInputArtifact();
|
||||
|
||||
@Override
|
||||
public void transform(TransformOutputs outputs) {
|
||||
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
|
||||
Map<String, String> automaticModules = getParameters().automaticModules;
|
||||
File originalJar = getInputArtifact().get().getAsFile();
|
||||
String originalJarName = originalJar.getName();
|
||||
|
||||
//Recreate jackson jars as open, non-synthetic modules
|
||||
if ((isModule(originalJar) && !originalJarName.contains("jackson")) || originalJarName.startsWith("javafx-")) {
|
||||
outputs.file(originalJar);
|
||||
} else if (moduleInfo.containsKey(originalJarName)) {
|
||||
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
|
||||
} else if (isAutoModule(originalJar)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isModule(File jar) {
|
||||
Pattern moduleInfoClassMrjarPath = Pattern.compile("META-INF/versions/\\d+/module-info.class");
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
|
||||
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
|
||||
ZipEntry next = inputStream.getNextEntry();
|
||||
while (next != null) {
|
||||
if ("module-info.class".equals(next.getName())) {
|
||||
return true;
|
||||
}
|
||||
if (isMultiReleaseJar && moduleInfoClassMrjarPath.matcher(next.getName()).matches()) {
|
||||
return true;
|
||||
}
|
||||
next = inputStream.getNextEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean containsMultiReleaseJarEntry(JarInputStream jarStream) {
|
||||
Manifest manifest = jarStream.getManifest();
|
||||
return manifest != null && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"));
|
||||
}
|
||||
|
||||
private boolean isAutoModule(File jar) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
|
||||
return inputStream.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private File getModuleJar(TransformOutputs outputs, File originalJar) {
|
||||
return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-module.jar");
|
||||
}
|
||||
|
||||
private static void addAutomaticModuleName(File originalJar, File moduleJar, String moduleName) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
|
||||
Manifest manifest = inputStream.getManifest();
|
||||
manifest.getMainAttributes().put(new Attributes.Name("Automatic-Module-Name"), moduleName);
|
||||
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
|
||||
copyEntries(inputStream, outputStream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
|
||||
Manifest manifest = inputStream.getManifest();
|
||||
if(manifest == null) {
|
||||
manifest = new Manifest();
|
||||
}
|
||||
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), manifest)) {
|
||||
copyEntries(inputStream, outputStream);
|
||||
outputStream.putNextEntry(new JarEntry("module-info.class"));
|
||||
outputStream.write(addModuleInfo(moduleInfo));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
|
||||
JarEntry jarEntry = inputStream.getNextJarEntry();
|
||||
while (jarEntry != null) {
|
||||
if(!jarEntry.getName().equals("module-info.class")) {
|
||||
outputStream.putNextEntry(jarEntry);
|
||||
outputStream.write(inputStream.readAllBytes());
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
jarEntry = inputStream.getNextJarEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] addModuleInfo(ModuleInfo moduleInfo) {
|
||||
ClassWriter classWriter = new ClassWriter(0);
|
||||
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
|
||||
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.getModuleVersion());
|
||||
for (String packageName : moduleInfo.getExports()) {
|
||||
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
|
||||
}
|
||||
moduleVisitor.visitRequire("java.base", 0, null);
|
||||
for (String requireName : moduleInfo.getRequires()) {
|
||||
moduleVisitor.visitRequire(requireName, 0, null);
|
||||
}
|
||||
for (String requireName : moduleInfo.getRequiresTransitive()) {
|
||||
moduleVisitor.visitRequire(requireName, Opcodes.ACC_TRANSITIVE, null);
|
||||
}
|
||||
for (String usesName : moduleInfo.getUses()) {
|
||||
moduleVisitor.visitUse(usesName.replace('.', '/'));
|
||||
}
|
||||
moduleVisitor.visitEnd();
|
||||
classWriter.visitEnd();
|
||||
return classWriter.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data class to hold the information that should be added as module-info.class to an existing Jar file.
|
||||
*/
|
||||
public class ModuleInfo implements Serializable {
|
||||
private String moduleName;
|
||||
private String moduleVersion;
|
||||
private List<String> exports = new ArrayList<>();
|
||||
private List<String> requires = new ArrayList<>();
|
||||
private List<String> requiresTransitive = new ArrayList<>();
|
||||
private List<String> uses = new ArrayList<>();
|
||||
|
||||
ModuleInfo(String moduleName, String moduleVersion) {
|
||||
this.moduleName = moduleName;
|
||||
this.moduleVersion = moduleVersion;
|
||||
}
|
||||
|
||||
public void exports(String exports) {
|
||||
this.exports.add(exports);
|
||||
}
|
||||
|
||||
public void requires(String requires) {
|
||||
this.requires.add(requires);
|
||||
}
|
||||
|
||||
public void requiresTransitive(String requiresTransitive) {
|
||||
this.requiresTransitive.add(requiresTransitive);
|
||||
}
|
||||
|
||||
public void uses(String uses) {
|
||||
this.uses.add(uses);
|
||||
}
|
||||
|
||||
public String getModuleName() {
|
||||
return moduleName;
|
||||
}
|
||||
|
||||
protected String getModuleVersion() {
|
||||
return moduleVersion;
|
||||
}
|
||||
|
||||
protected List<String> getExports() {
|
||||
return exports;
|
||||
}
|
||||
|
||||
protected List<String> getRequires() {
|
||||
return requires;
|
||||
}
|
||||
|
||||
protected List<String> getRequiresTransitive() {
|
||||
return requiresTransitive;
|
||||
}
|
||||
|
||||
protected List<String> getUses() {
|
||||
return uses;
|
||||
}
|
||||
}
|
||||
+10
-9
@@ -12,17 +12,18 @@ 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.6.6 and later, this is Eclipse Temurin 18.0.1+10.
|
||||
For v1.6.6 to v1.9.1, this was Eclipse Temurin 18.0.1+10. For v2.0.0 and later, Eclipse Temurin 22.0.2+9 is used.
|
||||
|
||||
#### Java from Adoptium github repo
|
||||
|
||||
It is available for all supported platforms from [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
|
||||
It is available for all supported platforms from [Eclipse Temurin 22.0.2+9](https://github.com/adoptium/temurin22-binaries/releases/tag/jdk-22.0.2%2B9).
|
||||
|
||||
For reference, the downloads are as follows:
|
||||
- [Linux x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz)
|
||||
- [MacOS x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_mac_hotspot_18.0.1_10.tar.gz)
|
||||
- [MacOS aarch64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_aarch64_mac_hotspot_18.0.1_10.tar.gz)
|
||||
- [Windows x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_windows_hotspot_18.0.1_10.zip)
|
||||
- [Linux x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_linux_hotspot_22.0.2_9.tar.gz)
|
||||
- [Linux aarch64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_linux_hotspot_22.0.2_9.tar.gz)
|
||||
- [MacOS x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_mac_hotspot_22.0.2_9.tar.gz)
|
||||
- [MacOS aarch64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_aarch64_mac_hotspot_22.0.2_9.tar.gz)
|
||||
- [Windows x64](https://github.com/adoptium/temurin22-binaries/releases/download/jdk-22.0.2%2B9/OpenJDK22U-jdk_x64_windows_hotspot_22.0.2_9.zip)
|
||||
|
||||
#### Java from Adoptium deb repo
|
||||
|
||||
@@ -57,7 +58,7 @@ echo "deb [signed-by=/usr/share/keyrings/adoptium.asc] https://packages.adoptium
|
||||
Update cache, install the desired temurin version and configure java to be linked to this same version:
|
||||
```
|
||||
sudo apt update -y
|
||||
sudo apt-get install -y temurin-18-jdk=18.0.1+10
|
||||
sudo apt-get install -y temurin-22-jdk=22.0.2+9
|
||||
sudo update-alternatives --config java
|
||||
```
|
||||
|
||||
@@ -67,7 +68,7 @@ A alternative option for all platforms is to use the [sdkman.io](https://sdkman.
|
||||
See the installation [instructions here](https://sdkman.io/install).
|
||||
Once installed, run
|
||||
```shell
|
||||
sdk install java 18.0.1-tem
|
||||
sdk install java 22.0.2-tem
|
||||
```
|
||||
|
||||
### Other requirements
|
||||
@@ -82,7 +83,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||
|
||||
```shell
|
||||
GIT_TAG="1.8.4"
|
||||
GIT_TAG="2.0.0"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
||||
+1
-1
Submodule drongo updated: b128bb895d...f7d5b4fb8f
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
Submodule
+1
Submodule lark added at c4c7ca8da3
@@ -1,3 +1,4 @@
|
||||
rootProject.name = 'sparrow'
|
||||
include 'drongo'
|
||||
include 'lark'
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
Package: sparrow
|
||||
Version: 2.1.2-1
|
||||
Section: utils
|
||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||
Priority: optional
|
||||
Architecture: arm64
|
||||
Provides: sparrow
|
||||
Description: Sparrow
|
||||
Depends: libc6, zlib1g
|
||||
@@ -0,0 +1,9 @@
|
||||
Package: sparrow
|
||||
Version: 2.1.2-1
|
||||
Section: utils
|
||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Provides: sparrow
|
||||
Description: Sparrow
|
||||
Depends: libc6, zlib1g
|
||||
@@ -8,3 +8,4 @@ Type=Application
|
||||
Categories=Finance;Network;
|
||||
MimeType=application/psbt;application/bitcoin-transaction;application/pgp-signature;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||
StartupWMClass=Sparrow
|
||||
SingleMainWindow=true
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
# postinst script for sparrow
|
||||
#
|
||||
# see: dh_installdeb(1)
|
||||
|
||||
set -e
|
||||
|
||||
# summary of how this script can be called:
|
||||
# * <postinst> `configure' <most-recently-configured-version>
|
||||
# * <old-postinst> `abort-upgrade' <new version>
|
||||
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
|
||||
# <new-version>
|
||||
# * <postinst> `abort-remove'
|
||||
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
|
||||
# <failed-install-package> <version> `removing'
|
||||
# <conflicting-package> <version>
|
||||
# for details, see https://www.debian.org/doc/debian-policy/ or
|
||||
# the debian-policy package
|
||||
|
||||
package_type=deb
|
||||
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop
|
||||
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
||||
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
if ! getent group plugdev > /dev/null; then
|
||||
groupadd plugdev
|
||||
fi
|
||||
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
|
||||
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
|
||||
fi
|
||||
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
|
||||
/bin/udevadm control --reload
|
||||
/bin/udevadm trigger
|
||||
fi
|
||||
;;
|
||||
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "postinst called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
Executable
+259
@@ -0,0 +1,259 @@
|
||||
Summary: Sparrow
|
||||
Name: sparrow
|
||||
Version: 2.1.2
|
||||
Release: 1
|
||||
License: ASL 2.0
|
||||
Vendor: Unknown
|
||||
|
||||
%if "x" != "x"
|
||||
URL:
|
||||
%endif
|
||||
|
||||
%if "x/opt" != "x"
|
||||
Prefix: /opt
|
||||
%endif
|
||||
|
||||
Provides: sparrow
|
||||
|
||||
%if "xutils" != "x"
|
||||
Group: utils
|
||||
%endif
|
||||
|
||||
Autoprov: 0
|
||||
Autoreq: 0
|
||||
%if "xxdg-utils" != "x" || "x" != "x"
|
||||
Requires: xdg-utils
|
||||
%endif
|
||||
|
||||
#comment line below to enable effective jar compression
|
||||
#it could easily get your package size from 40 to 15Mb but
|
||||
#build time will substantially increase and it may require unpack200/system java to install
|
||||
%define __jar_repack %{nil}
|
||||
|
||||
# on RHEL we got unwanted improved debugging enhancements
|
||||
%define _build_id_links none
|
||||
|
||||
%define package_filelist %{_builddir}/%{name}.files
|
||||
%define app_filelist %{_builddir}/%{name}.app.files
|
||||
%define filesystem_filelist %{_builddir}/%{name}.filesystem.files
|
||||
|
||||
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
|
||||
|
||||
%description
|
||||
Sparrow
|
||||
|
||||
%global __os_install_post %{nil}
|
||||
|
||||
%prep
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
install -d -m 755 %{buildroot}/opt/sparrow
|
||||
cp -r %{_sourcedir}/opt/sparrow/* %{buildroot}/opt/sparrow
|
||||
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
||||
install -d -m 755 %{buildroot}/lib/systemd/system
|
||||
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
||||
fi
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
%define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE}
|
||||
install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}"
|
||||
install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}"
|
||||
%endif
|
||||
(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist}
|
||||
{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist}
|
||||
comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist}
|
||||
sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
||||
(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist}
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist}
|
||||
%endif
|
||||
|
||||
%files -f %{package_filelist}
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
%license "%{license_install_file}"
|
||||
%endif
|
||||
|
||||
%post
|
||||
package_type=rpm
|
||||
xdg-desktop-menu install /opt/sparrow/lib/sparrow-Sparrow.desktop
|
||||
xdg-mime install /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
||||
install -D -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
if ! getent group plugdev > /dev/null; then
|
||||
groupadd plugdev
|
||||
fi
|
||||
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
|
||||
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
|
||||
fi
|
||||
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
|
||||
/bin/udevadm control --reload
|
||||
/bin/udevadm trigger
|
||||
fi
|
||||
|
||||
%pre
|
||||
package_type=rpm
|
||||
file_belongs_to_single_package ()
|
||||
{
|
||||
if [ ! -e "$1" ]; then
|
||||
false
|
||||
elif [ "$package_type" = rpm ]; then
|
||||
test `rpm -q --whatprovides "$1" | wc -l` = 1
|
||||
elif [ "$package_type" = deb ]; then
|
||||
test `dpkg -S "$1" | wc -l` = 1
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
do_if_file_belongs_to_single_package ()
|
||||
{
|
||||
local file="$1"
|
||||
shift
|
||||
|
||||
if file_belongs_to_single_package "$file"; then
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$1" -gt 1 ]; then
|
||||
:;
|
||||
fi
|
||||
|
||||
%preun
|
||||
package_type=rpm
|
||||
file_belongs_to_single_package ()
|
||||
{
|
||||
if [ ! -e "$1" ]; then
|
||||
false
|
||||
elif [ "$package_type" = rpm ]; then
|
||||
test `rpm -q --whatprovides "$1" | wc -l` = 1
|
||||
elif [ "$package_type" = deb ]; then
|
||||
test `dpkg -S "$1" | wc -l` = 1
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
do_if_file_belongs_to_single_package ()
|
||||
{
|
||||
local file="$1"
|
||||
shift
|
||||
|
||||
if file_belongs_to_single_package "$file"; then
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
#
|
||||
# Remove $1 desktop file from the list of default handlers for $2 mime type
|
||||
# in $3 file dumping output to stdout.
|
||||
#
|
||||
desktop_filter_out_default_mime_handler ()
|
||||
{
|
||||
local defaults_list="$3"
|
||||
|
||||
local desktop_file="$1"
|
||||
local mime_type="$2"
|
||||
|
||||
awk -f- "$defaults_list" <<EOF
|
||||
BEGIN {
|
||||
mime_type="$mime_type"
|
||||
mime_type_regexp="~" mime_type "="
|
||||
desktop_file="$desktop_file"
|
||||
}
|
||||
\$0 ~ mime_type {
|
||||
\$0 = substr(\$0, length(mime_type) + 2);
|
||||
split(\$0, desktop_files, ";")
|
||||
remaining_desktop_files
|
||||
counter=0
|
||||
for (idx in desktop_files) {
|
||||
if (desktop_files[idx] != desktop_file) {
|
||||
++counter;
|
||||
}
|
||||
}
|
||||
if (counter) {
|
||||
printf mime_type "="
|
||||
for (idx in desktop_files) {
|
||||
if (desktop_files[idx] != desktop_file) {
|
||||
printf desktop_files[idx]
|
||||
if (--counter) {
|
||||
printf ";"
|
||||
}
|
||||
}
|
||||
}
|
||||
printf "\n"
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
{ print }
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Remove $2 desktop file from the list of default handlers for $@ mime types
|
||||
# in $1 file.
|
||||
# Result is saved in $1 file.
|
||||
#
|
||||
desktop_uninstall_default_mime_handler_0 ()
|
||||
{
|
||||
local defaults_list=$1
|
||||
shift
|
||||
[ -f "$defaults_list" ] || return 0
|
||||
|
||||
local desktop_file="$1"
|
||||
shift
|
||||
|
||||
tmpfile1=$(mktemp)
|
||||
tmpfile2=$(mktemp)
|
||||
cat "$defaults_list" > "$tmpfile1"
|
||||
|
||||
local v
|
||||
local update=
|
||||
for mime in "$@"; do
|
||||
desktop_filter_out_default_mime_handler "$desktop_file" "$mime" "$tmpfile1" > "$tmpfile2"
|
||||
v="$tmpfile2"
|
||||
tmpfile2="$tmpfile1"
|
||||
tmpfile1="$v"
|
||||
|
||||
if ! diff -q "$tmpfile1" "$tmpfile2" > /dev/null; then
|
||||
update=yes
|
||||
desktop_trace Remove $desktop_file default handler for $mime mime type from $defaults_list file
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$update" ]; then
|
||||
cat "$tmpfile1" > "$defaults_list"
|
||||
desktop_trace "$defaults_list" file updated
|
||||
fi
|
||||
|
||||
rm -f "$tmpfile1" "$tmpfile2"
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Remove $1 desktop file from the list of default handlers for $@ mime types
|
||||
# in all known system defaults lists.
|
||||
#
|
||||
desktop_uninstall_default_mime_handler ()
|
||||
{
|
||||
for f in /usr/share/applications/defaults.list /usr/local/share/applications/defaults.list; do
|
||||
desktop_uninstall_default_mime_handler_0 "$f" "$@"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
desktop_trace ()
|
||||
{
|
||||
echo "$@"
|
||||
}
|
||||
|
||||
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrow/lib/sparrow-Sparrow.desktop
|
||||
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrow/lib/sparrow-Sparrow-MimeInfo.xml
|
||||
do_if_file_belongs_to_single_package /opt/sparrow/lib/sparrow-Sparrow.desktop desktop_uninstall_default_mime_handler sparrow-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning
|
||||
|
||||
|
||||
%clean
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.8.5</string>
|
||||
<string>2.1.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
@@ -35,6 +35,8 @@
|
||||
<string>true</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Sparrow requires access to the camera in order to scan QR codes</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Sparrow requires access to the local network in order to connect to your configured server</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@@ -7,9 +7,7 @@ import com.google.common.io.ByteSource;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
@@ -25,12 +23,9 @@ import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.CounterpartyDialog;
|
||||
import com.sparrowwallet.sparrow.settings.SettingsGroup;
|
||||
import com.sparrowwallet.sparrow.settings.SettingsDialog;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionData;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
@@ -155,6 +150,10 @@ public class AppController implements Initializable {
|
||||
private CheckMenuItem useHdCameraResolution;
|
||||
private static final BooleanProperty useHdCameraResolutionProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem mirrorCameraImage;
|
||||
private static final BooleanProperty mirrorCameraImageProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem showLoadingLog;
|
||||
private static final BooleanProperty showLoadingLogProperty = new SimpleBooleanProperty();
|
||||
@@ -187,9 +186,6 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem sweepPrivateKey;
|
||||
|
||||
@FXML
|
||||
private MenuItem findMixingPartner;
|
||||
|
||||
@FXML
|
||||
private MenuItem showPayNym;
|
||||
|
||||
@@ -215,10 +211,14 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch serverToggle;
|
||||
|
||||
private Storage.KeyDerivationService keyDerivationService;
|
||||
|
||||
private PauseTransition wait;
|
||||
|
||||
private Timeline statusTimeline;
|
||||
|
||||
private SearchWalletDialog searchWalletDialog;
|
||||
|
||||
private SendToManyDialog sendToManyDialog;
|
||||
|
||||
private DownloadVerifierDialog downloadVerifierDialog;
|
||||
@@ -278,7 +278,16 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
void initializeView() {
|
||||
setPlatformApplicationMenu();
|
||||
Platform.runLater(this::setPlatformApplicationMenu);
|
||||
|
||||
rootStack.getScene().getWindow().setOnHiding(windowEvent -> {
|
||||
if(searchWalletDialog != null && searchWalletDialog.isShowing()) {
|
||||
searchWalletDialog.close();
|
||||
}
|
||||
if(sendToManyDialog != null && sendToManyDialog.isShowing()) {
|
||||
sendToManyDialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
rootStack.setOnDragOver(event -> {
|
||||
if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) {
|
||||
@@ -374,6 +383,8 @@ public class AppController implements Initializable {
|
||||
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
|
||||
useHdCameraResolutionProperty.set(Config.get().isHdCapture());
|
||||
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
|
||||
mirrorCameraImageProperty.set(Config.get().isMirrorCapture());
|
||||
mirrorCameraImage.selectedProperty().bindBidirectional(mirrorCameraImageProperty);
|
||||
showTxHexProperty.set(Config.get().isShowTransactionHex());
|
||||
showTxHex.selectedProperty().bindBidirectional(showTxHexProperty);
|
||||
showLoadingLogProperty.set(Config.get().isShowLoadingLog());
|
||||
@@ -384,7 +395,7 @@ public class AppController implements Initializable {
|
||||
MenuItem homeItem = new MenuItem("Home Folder...");
|
||||
homeItem.setOnAction(this::restartInHome);
|
||||
restart.getItems().add(homeItem);
|
||||
List<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET));
|
||||
List<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.TESTNET4, Network.SIGNET));
|
||||
networks.remove(Network.get());
|
||||
for(Network network : networks) {
|
||||
MenuItem networkItem = new MenuItem(network.toDisplayString());
|
||||
@@ -410,10 +421,6 @@ public class AppController implements Initializable {
|
||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||
sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()));
|
||||
showPayNym.setDisable(true);
|
||||
findMixingPartner.setDisable(true);
|
||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||
});
|
||||
|
||||
configureSwitchServer();
|
||||
setServerType(Config.get().getServerType());
|
||||
@@ -430,8 +437,8 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void registerShortcuts() {
|
||||
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
|
||||
if(platform == org.controlsfx.tools.Platform.OSX) {
|
||||
OsType osType = OsType.getCurrent();
|
||||
if(osType == OsType.MACOS) {
|
||||
tabs.getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if(event.isShortcutDown() && event.isAltDown() && (event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.RIGHT)) {
|
||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||
@@ -446,14 +453,14 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void setPlatformApplicationMenu() {
|
||||
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
|
||||
if(platform == org.controlsfx.tools.Platform.OSX) {
|
||||
OsType osType = OsType.getCurrent();
|
||||
if(osType == OsType.MACOS) {
|
||||
MenuToolkit tk = MenuToolkit.toolkit();
|
||||
MenuItem preferences = new MenuItem("Preferences...");
|
||||
preferences.setOnAction(this::openPreferences);
|
||||
preferences.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN));
|
||||
MenuItem settings = new MenuItem("Settings...");
|
||||
settings.setOnAction(this::openSettings);
|
||||
settings.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN));
|
||||
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(SparrowWallet.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
|
||||
preferences, new SeparatorMenuItem(),
|
||||
settings, new SeparatorMenuItem(),
|
||||
tk.createHideMenuItem(SparrowWallet.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
|
||||
tk.createQuitMenuItem(SparrowWallet.APP_NAME));
|
||||
tk.setApplicationMenu(defaultApplicationMenu);
|
||||
@@ -461,11 +468,11 @@ public class AppController implements Initializable {
|
||||
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||
helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
} else if(osType == OsType.WINDOWS) {
|
||||
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("windowsHide"));
|
||||
}
|
||||
|
||||
if(platform == org.controlsfx.tools.Platform.UNIX || !TrayManager.isSupported()) {
|
||||
if(osType == OsType.UNIX || !TrayManager.isSupported()) {
|
||||
viewMenu.getItems().remove(minimizeToTray);
|
||||
}
|
||||
}
|
||||
@@ -491,7 +498,7 @@ public class AppController implements Initializable {
|
||||
welcomeDialog.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
|
||||
if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) {
|
||||
openPreferences(PreferenceGroup.SERVER);
|
||||
openSettings(SettingsGroup.SERVER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,7 +544,7 @@ public class AppController implements Initializable {
|
||||
StackPane root = loader.load();
|
||||
AboutController controller = loader.getController();
|
||||
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
if(OsType.getCurrent() == OsType.WINDOWS) {
|
||||
root.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||
}
|
||||
|
||||
@@ -565,21 +572,27 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
public void installUdevRules(ActionEvent event) {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
Platform.runLater(this::showInstallUdevMessage);
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
Platform.runLater(this::showInstallUdevMessage);
|
||||
});
|
||||
enumerateService.start();
|
||||
}
|
||||
String commands = """
|
||||
sudo install -m 644 /opt/sparrow/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
sudo udevadm control --reload
|
||||
sudo udevadm trigger
|
||||
sudo groupadd -f plugdev
|
||||
sudo usermod -aG plugdev `whoami`
|
||||
""";
|
||||
String home = System.getProperty(JPACKAGE_APP_PATH);
|
||||
if(home != null && !home.startsWith("/opt/sparrow") && home.endsWith("bin/Sparrow")) {
|
||||
home = home.replace("bin/Sparrow", "");
|
||||
commands = commands.replace("/opt/sparrow/", home);
|
||||
}
|
||||
|
||||
public void showInstallUdevMessage() {
|
||||
TextAreaDialog dialog = new TextAreaDialog("sudo " + Config.get().getHwi().getAbsolutePath() + " installudevrules", false);
|
||||
TextAreaDialog dialog = new TextAreaDialog(commands, false);
|
||||
dialog.initOwner(rootStack.getScene().getWindow());
|
||||
dialog.setTitle("Install Udev Rules");
|
||||
dialog.getDialogPane().setHeaderText("Installing udev rules ensures devices can connect over USB.\nThis command requires root privileges.\nOpen a shell and enter the following:");
|
||||
dialog.setTitle("Install udev Rules");
|
||||
dialog.getDialogPane().setHeaderText("""
|
||||
Installing udev rules ensures devices can connect over USB.
|
||||
These commands require root privileges.
|
||||
Open a shell and enter the following.
|
||||
""");
|
||||
dialog.showAndWait();
|
||||
}
|
||||
|
||||
@@ -589,7 +602,7 @@ public class AppController implements Initializable {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Transaction");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("PSBT", "*.psbt"),
|
||||
new FileChooser.ExtensionFilter("TXN", "*.txn")
|
||||
);
|
||||
@@ -620,19 +633,10 @@ public class AppController implements Initializable {
|
||||
byte[] bytes = Files.readAllBytes(file.toPath());
|
||||
String name = file.getName();
|
||||
|
||||
try {
|
||||
if(Utils.isHex(bytes) || Utils.isBase64(bytes)) {
|
||||
addTransactionTab(name, file, new String(bytes, StandardCharsets.UTF_8).trim());
|
||||
} else {
|
||||
addTransactionTab(name, file, bytes);
|
||||
} catch(ParseException e) {
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
|
||||
ByteSource byteSource = new ByteSource() {
|
||||
@Override
|
||||
public InputStream openStream() {
|
||||
return inputStream;
|
||||
}
|
||||
};
|
||||
|
||||
String text = byteSource.asCharSource(Charsets.UTF_8).read().trim();
|
||||
addTransactionTab(name, file, text);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
showErrorDialog("Error opening file", e.getMessage());
|
||||
@@ -797,6 +801,7 @@ public class AppController implements Initializable {
|
||||
|
||||
String fileName = ((Label)selectedTab.getGraphic()).getText();
|
||||
if(fileName != null && !fileName.isEmpty()) {
|
||||
fileName = fileName.replace('/', '_');
|
||||
if(!fileName.endsWith(".psbt")) {
|
||||
fileName += ".psbt";
|
||||
}
|
||||
@@ -944,6 +949,11 @@ public class AppController implements Initializable {
|
||||
Config.get().setHdCapture(item.isSelected());
|
||||
}
|
||||
|
||||
public void mirrorCameraImage(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setMirrorCapture(item.isSelected());
|
||||
}
|
||||
|
||||
public void showLoadingLog(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setShowLoadingLog(item.isSelected());
|
||||
@@ -1311,7 +1321,7 @@ public class AppController implements Initializable {
|
||||
log.error("Error saving imported wallet", e);
|
||||
}
|
||||
} else {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
||||
keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
@@ -1342,11 +1352,13 @@ public class AppController implements Initializable {
|
||||
if(key != null) {
|
||||
key.clear();
|
||||
}
|
||||
keyDerivationService = null;
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Failed"));
|
||||
showErrorDialog("Error encrypting wallet", keyDerivationService.getException().getMessage());
|
||||
keyDerivationService = null;
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.START, "Encrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
@@ -1366,18 +1378,18 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void openPreferences(ActionEvent event) {
|
||||
openPreferences(PreferenceGroup.GENERAL);
|
||||
public void openSettings(ActionEvent event) {
|
||||
openSettings(SettingsGroup.GENERAL);
|
||||
}
|
||||
|
||||
public void openServerPreferences(ActionEvent event) {
|
||||
openPreferences(PreferenceGroup.SERVER);
|
||||
public void openServerSettings(ActionEvent event) {
|
||||
openSettings(SettingsGroup.SERVER);
|
||||
}
|
||||
|
||||
private void openPreferences(PreferenceGroup preferenceGroup) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(preferenceGroup);
|
||||
preferencesDialog.initOwner(rootStack.getScene().getWindow());
|
||||
preferencesDialog.showAndWait();
|
||||
private void openSettings(SettingsGroup settingsGroup) {
|
||||
SettingsDialog settingsDialog = new SettingsDialog(settingsGroup);
|
||||
settingsDialog.initOwner(rootStack.getScene().getWindow());
|
||||
settingsDialog.showAndWait();
|
||||
configureSwitchServer();
|
||||
serverToggle.setDisable(!Config.get().hasServer());
|
||||
}
|
||||
@@ -1419,7 +1431,6 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit);
|
||||
sendToManyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
sendToManyDialog.initModality(Modality.NONE);
|
||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||
sendToManyDialog = null;
|
||||
@@ -1445,74 +1456,6 @@ public class AppController implements Initializable {
|
||||
optTransaction.ifPresent(transaction -> addTransactionTab(null, null, transaction));
|
||||
}
|
||||
|
||||
public void findMixingPartner(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(selectedWalletForm.getWalletId());
|
||||
if(soroban.getHdWallet() == null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
dlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage storage = selectedWalletForm.getStorage();
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
copy.decrypt(key);
|
||||
|
||||
try {
|
||||
soroban.setHDWallet(copy);
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
} finally {
|
||||
key.clear();
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(() -> findMixingPartner(null));
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
soroban.setHDWallet(wallet);
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
}
|
||||
} else {
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showPayNym(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
@@ -1605,13 +1548,46 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void searchWallets(List<WalletForm> walletForms) {
|
||||
SearchWalletDialog searchWalletDialog = new SearchWalletDialog(walletForms);
|
||||
searchWalletDialog.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<Entry> optEntry = searchWalletDialog.showAndWait();
|
||||
if(optEntry.isPresent()) {
|
||||
Entry entry = optEntry.get();
|
||||
EventManager.get().post(new FunctionActionEvent(entry.getWalletFunction(), entry.getWallet()));
|
||||
Platform.runLater(() -> EventManager.get().post(new SelectEntryEvent(entry)));
|
||||
if(searchWalletDialog != null) {
|
||||
if(!searchWalletDialog.getWalletForms().equals(walletForms)) {
|
||||
searchWalletDialog.close();
|
||||
} else {
|
||||
Stage stage = (Stage)searchWalletDialog.getDialogPane().getScene().getWindow();
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.setAlwaysOnTop(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
searchWalletDialog = new SearchWalletDialog(walletForms);
|
||||
searchWalletDialog.initModality(Modality.NONE);
|
||||
Optional<Entry> optEntry = searchWalletDialog.showAndWait();
|
||||
if(optEntry.isPresent()) {
|
||||
Entry entry = optEntry.get();
|
||||
EventManager.get().post(new FunctionActionEvent(entry.getWalletFunction(), entry.getWallet()));
|
||||
Platform.runLater(() -> EventManager.get().post(new SelectEntryEvent(entry)));
|
||||
}
|
||||
searchWalletDialog = null;
|
||||
});
|
||||
}
|
||||
|
||||
public void showAllWalletsSummary(ActionEvent event) {
|
||||
List<List<WalletForm>> allWalletForms = new ArrayList<>();
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)tab.getContent();
|
||||
allWalletForms.add(subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm())
|
||||
.filter(walletForm -> walletForm.getWallet().isValid() && !walletForm.isLocked()).collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
if(allWalletForms.isEmpty() || allWalletForms.stream().allMatch(List::isEmpty)) {
|
||||
showErrorDialog("No wallets", "There are no open and unlocked wallets to summarize.");
|
||||
} else {
|
||||
WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(allWalletForms);
|
||||
walletSummaryDialog.initOwner(rootStack.getScene().getWindow());
|
||||
walletSummaryDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1623,7 +1599,7 @@ public class AppController implements Initializable {
|
||||
TabPane subTabs = (TabPane) selectedTab.getContent();
|
||||
List<WalletForm> walletForms = subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm()).collect(Collectors.toList());
|
||||
if(!walletForms.isEmpty()) {
|
||||
WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(walletForms);
|
||||
WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(List.of(walletForms));
|
||||
walletSummaryDialog.initOwner(rootStack.getScene().getWindow());
|
||||
walletSummaryDialog.showAndWait();
|
||||
}
|
||||
@@ -1679,14 +1655,6 @@ public class AppController implements Initializable {
|
||||
tabLabel.setGraphicTextGap(5.0);
|
||||
tab.setGraphic(tabLabel);
|
||||
tab.setClosable(true);
|
||||
tab.setOnCloseRequest(event -> {
|
||||
if(AppServices.getWhirlpoolServices().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) {
|
||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Close mix to wallet?", "This wallet has been configured as the final destination for mixes, and needs to be open for this to occur.\n\nAre you sure you want to close?", ButtonType.NO, ButtonType.YES);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.NO) {
|
||||
event.consume();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TabPane subTabs = new TabPane();
|
||||
subTabs.setSide(Side.LEFT);
|
||||
@@ -1913,6 +1881,25 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void addTransactionTab(String name, File file, PSBT psbt) {
|
||||
//Add any missing previous outputs if available in open wallets
|
||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||
if(psbtInput.getUtxo() == null) {
|
||||
for(Wallet wallet : AppServices.get().getOpenWallets().keySet().stream().filter(Wallet::isValid).toList()) {
|
||||
TransactionOutPoint outpoint = psbtInput.getInput().getOutpoint();
|
||||
BlockTransaction blockTransaction = wallet.getWalletTransaction(outpoint.getHash());
|
||||
if(blockTransaction != null && blockTransaction.getTransaction().getOutputs().size() > outpoint.getIndex()) {
|
||||
psbtInput.setNonWitnessUtxo(blockTransaction.getTransaction());
|
||||
ScriptType type = psbtInput.getScriptType();
|
||||
if(type != null && Arrays.asList(ScriptType.WITNESS_TYPES).contains(type)) {
|
||||
psbtInput.setWitnessUtxo(blockTransaction.getTransaction().getOutputs().get((int)outpoint.getIndex()));
|
||||
psbtInput.setNonWitnessUtxo(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
|
||||
if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) {
|
||||
EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt));
|
||||
@@ -1930,7 +1917,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void addTransactionTab(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||
addTransactionTab(null, null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex);
|
||||
addTransactionTab(blockTransaction.getLabel(), null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex);
|
||||
}
|
||||
|
||||
private void addTransactionTab(String name, File file, Transaction transaction, PSBT psbt, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||
@@ -2119,7 +2106,7 @@ public class AppController implements Initializable {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Image");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("Images", "*.png", "*.jpg", "*.jpeg", "*.gif")
|
||||
);
|
||||
|
||||
@@ -2180,7 +2167,7 @@ public class AppController implements Initializable {
|
||||
dlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
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();
|
||||
@@ -2190,7 +2177,7 @@ public class AppController implements Initializable {
|
||||
deleteStorage(storage, true);
|
||||
} finally {
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
keyDerivationService = null;
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
@@ -2203,6 +2190,7 @@ public class AppController implements Initializable {
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
keyDerivationService = null;
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
@@ -2505,7 +2493,6 @@ public class AppController implements Initializable {
|
||||
showLoadingLog.setDisable(true);
|
||||
showTxHex.setDisable(false);
|
||||
showPayNym.setDisable(true);
|
||||
findMixingPartner.setDisable(true);
|
||||
} else if(event instanceof WalletTabSelectedEvent) {
|
||||
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
|
||||
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
|
||||
@@ -2517,7 +2504,6 @@ public class AppController implements Initializable {
|
||||
showLoadingLog.setDisable(false);
|
||||
showTxHex.setDisable(true);
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2543,7 +2529,6 @@ public class AppController implements Initializable {
|
||||
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
||||
exportWallet.setDisable(!event.getWallet().isValid() || selectedWalletForm.isLocked());
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !event.getWallet().hasPaymentCode());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2764,8 +2749,8 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void connectionFailed(ConnectionFailedEvent event) {
|
||||
String status = CONNECTION_FAILED_PREFIX + event.getMessage();
|
||||
Hyperlink hyperlink = new Hyperlink("Server Preferences");
|
||||
hyperlink.setOnAction(this::openServerPreferences);
|
||||
Hyperlink hyperlink = new Hyperlink("Server Settings");
|
||||
hyperlink.setOnAction(this::openServerSettings);
|
||||
statusUpdated(new StatusEvent(status, hyperlink));
|
||||
serverToggleStopAnimation();
|
||||
setTorIcon();
|
||||
@@ -3075,6 +3060,8 @@ public class AppController implements Initializable {
|
||||
public void requestWalletOpen(RequestWalletOpenEvent event) {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
if(event.getFile() != null) {
|
||||
Optional<Tab> optExisting = tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData walletTabData && walletTabData.getStorage().getWalletFile().equals(event.getFile())).findFirst();
|
||||
optExisting.ifPresent(tab -> tabs.getTabs().remove(tab));
|
||||
openWalletFile(event.getFile(), true);
|
||||
} else {
|
||||
openWallet(true);
|
||||
@@ -3157,4 +3144,14 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void webcamResolutionChanged(WebcamResolutionChangedEvent event) {
|
||||
useHdCameraResolutionProperty.set(event.isHdResolution());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void webcamMirroredChanged(WebcamMirroredChangedEvent event) {
|
||||
mirrorCameraImageProperty.set(event.isMirrored());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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.OsType;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
@@ -24,8 +25,6 @@ import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@@ -95,11 +94,7 @@ public class AppServices {
|
||||
|
||||
private static AppServices INSTANCE;
|
||||
|
||||
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
|
||||
|
||||
private final SorobanServices sorobanServices = new SorobanServices();
|
||||
|
||||
private InteractionServices interactionServices;
|
||||
private final InteractionServices interactionServices;
|
||||
|
||||
private static HttpClientService httpClientService;
|
||||
|
||||
@@ -188,8 +183,6 @@ public class AppServices {
|
||||
this.application = application;
|
||||
this.interactionServices = interactionServices;
|
||||
EventManager.get().register(this);
|
||||
EventManager.get().register(whirlpoolServices);
|
||||
EventManager.get().register(sorobanServices);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -301,7 +294,7 @@ public class AppServices {
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
if(event instanceof ConnectionEvent && Network.get().equals(Network.MAINNET) && feeRatesSource.isExternal()) {
|
||||
if(event instanceof ConnectionEvent && feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) {
|
||||
EventManager.get().post(new FeeRatesSourceChangedEvent(feeRatesSource));
|
||||
}
|
||||
});
|
||||
@@ -387,7 +380,7 @@ public class AppServices {
|
||||
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
|
||||
currency == null ? DEFAULT_FIAT_CURRENCY : currency);
|
||||
//Delay startup on first run, Windows requires a longer delay
|
||||
ratesService.setDelay(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS ? Duration.seconds(RATES_DELAY_SECS_WINDOWS) : Duration.seconds(RATES_DELAY_SECS_DEFAULT));
|
||||
ratesService.setDelay(OsType.getCurrent() == OsType.WINDOWS ? Duration.seconds(RATES_DELAY_SECS_WINDOWS) : Duration.seconds(RATES_DELAY_SECS_DEFAULT));
|
||||
ratesService.setPeriod(Duration.seconds(RATES_PERIOD_SECS));
|
||||
ratesService.setRestartOnFailure(true);
|
||||
|
||||
@@ -502,7 +495,7 @@ public class AppServices {
|
||||
public static Proxy getProxy(String proxyCircuitId) {
|
||||
Config config = Config.get();
|
||||
Proxy proxy = null;
|
||||
if(config.isUseProxy()) {
|
||||
if(config.isUseProxy() && config.getProxyServer() != null) {
|
||||
HostAndPort proxyHostAndPort = HostAndPort.fromString(config.getProxyServer());
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
||||
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
@@ -534,14 +527,6 @@ public class AppServices {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static WhirlpoolServices getWhirlpoolServices() {
|
||||
return get().whirlpoolServices;
|
||||
}
|
||||
|
||||
public static SorobanServices getSorobanServices() {
|
||||
return get().sorobanServices;
|
||||
}
|
||||
|
||||
public static InteractionServices getInteractionServices() {
|
||||
return get().interactionServices;
|
||||
}
|
||||
@@ -589,6 +574,34 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
public static void runAfterDelay(long delay, Runnable runnable) {
|
||||
if(delay <= 0) {
|
||||
if(Platform.isFxApplicationThread()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
Platform.runLater(runnable);
|
||||
}
|
||||
} else {
|
||||
ScheduledService<Void> delayService = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
delayService.setOnSucceeded(_ -> {
|
||||
delayService.cancel();
|
||||
runnable.run();
|
||||
});
|
||||
delayService.setDelay(Duration.millis(delay));
|
||||
delayService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private static Image getWindowIcon() {
|
||||
if(windowIcon == null) {
|
||||
windowIcon = new Image(SparrowWallet.class.getResourceAsStream("/image/sparrow-icon.png"));
|
||||
@@ -607,7 +620,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
private static double getReducedWindowHeight() {
|
||||
return org.controlsfx.tools.Platform.getCurrent() != org.controlsfx.tools.Platform.OSX ? 802d : 768d; //Check for menu bar of ~34px
|
||||
return OsType.getCurrent() != OsType.MACOS ? 802d : 768d; //Check for menu bar of ~34px
|
||||
}
|
||||
|
||||
public Application getApplication() {
|
||||
@@ -923,6 +936,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
if(openWindow instanceof Stage) {
|
||||
((Stage)openWindow).setIconified(false);
|
||||
((Stage)openWindow).setAlwaysOnTop(true);
|
||||
((Stage)openWindow).setAlwaysOnTop(false);
|
||||
}
|
||||
@@ -1094,10 +1108,50 @@ public class AppServices {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public static final List<Network> WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
|
||||
public static boolean isWhirlpoolCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().get(0).hasSeed()
|
||||
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
|
||||
&& wallet.getStandardAccountType() != null
|
||||
&& StandardAccount.isMixableAccount(wallet.getStandardAccountType());
|
||||
}
|
||||
|
||||
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1;
|
||||
}
|
||||
|
||||
public static List<Wallet> addWhirlpoolWallets(Wallet decryptedWallet, String walletId, Storage storage) {
|
||||
List<Wallet> childWallets = new ArrayList<>();
|
||||
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
|
||||
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
|
||||
Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount);
|
||||
childWallets.add(childWallet);
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(storage, decryptedWallet, childWallet));
|
||||
}
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
public static Font getMonospaceFont() {
|
||||
return Font.font("Roboto Mono", 13);
|
||||
}
|
||||
|
||||
public static boolean isOnWayland() {
|
||||
if(OsType.getCurrent() != OsType.UNIX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String waylandDisplay = System.getenv("WAYLAND_DISPLAY");
|
||||
return waylandDisplay != null && !waylandDisplay.isEmpty();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newConnection(ConnectionEvent event) {
|
||||
currentBlockHeight = event.getBlockHeight();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
@@ -48,7 +49,7 @@ public class DefaultInteractionServices implements InteractionServices {
|
||||
}
|
||||
|
||||
String[] lines = content.split("\r\n|\r|\n");
|
||||
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
if(lines.length > 3 || OsType.getCurrent() == OsType.WINDOWS) {
|
||||
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
|
||||
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.control.WalletIcon;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
@@ -9,13 +10,12 @@ import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.settings.SettingsGroup;
|
||||
import com.sparrowwallet.sparrow.settings.SettingsDialog;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.glyphfont.GlyphFontRegistry;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
@@ -42,10 +42,7 @@ public class SparrowDesktop extends Application {
|
||||
public void start(Stage stage) throws Exception {
|
||||
this.mainStage = stage;
|
||||
|
||||
GlyphFontRegistry.register(new FontAwesome5());
|
||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
|
||||
initializeFonts();
|
||||
URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null);
|
||||
|
||||
AppServices.initialize(this);
|
||||
@@ -60,8 +57,8 @@ public class SparrowDesktop extends Application {
|
||||
Config.get().setMode(mode);
|
||||
|
||||
if(mode.equals(Mode.ONLINE)) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
|
||||
Optional<Boolean> optNewWallet = preferencesDialog.showAndWait();
|
||||
SettingsDialog settingsDialog = new SettingsDialog(SettingsGroup.SERVER, true);
|
||||
Optional<Boolean> optNewWallet = settingsDialog.showAndWait();
|
||||
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
|
||||
} else if(Network.get() == Network.MAINNET) {
|
||||
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
|
||||
@@ -75,7 +72,7 @@ public class SparrowDesktop extends Application {
|
||||
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
|
||||
}
|
||||
|
||||
if(Config.get().getHdCapture() == null && Platform.getCurrent() == Platform.OSX) {
|
||||
if(Config.get().getHdCapture() == null && OsType.getCurrent() == OsType.MACOS) {
|
||||
Config.get().setHdCapture(Boolean.TRUE);
|
||||
}
|
||||
|
||||
@@ -89,28 +86,42 @@ public class SparrowDesktop extends Application {
|
||||
|
||||
AppController appController = AppServices.newAppWindow(stage);
|
||||
|
||||
if(createNewWallet) {
|
||||
appController.newWallet(null);
|
||||
}
|
||||
final boolean showNewWallet = createNewWallet;
|
||||
//Delay opening new dialogs on Wayland
|
||||
AppServices.runAfterDelay(AppServices.isOnWayland() ? 1000 : 0, () -> {
|
||||
if(showNewWallet) {
|
||||
appController.newWallet(null);
|
||||
}
|
||||
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null) {
|
||||
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
||||
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
||||
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
||||
sortedWalletFiles.addAll(encryptedWalletFiles);
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null) {
|
||||
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
||||
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
||||
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
||||
sortedWalletFiles.addAll(encryptedWalletFiles);
|
||||
|
||||
for(File walletFile : sortedWalletFiles) {
|
||||
if(walletFile.exists()) {
|
||||
appController.openWalletFile(walletFile, false);
|
||||
for(File walletFile : sortedWalletFiles) {
|
||||
if(walletFile.exists()) {
|
||||
appController.openWalletFile(walletFile, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
|
||||
|
||||
AppServices.get().start();
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeFonts() {
|
||||
GlyphFontRegistry.register(new FontAwesome5());
|
||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
|
||||
}
|
||||
|
||||
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
|
||||
|
||||
AppServices.get().start();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.*;
|
||||
public class SparrowWallet {
|
||||
public static final String APP_ID = "sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.8.5";
|
||||
public static final String APP_VERSION = "2.1.2";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
@@ -66,6 +66,11 @@ public class SparrowWallet {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
File testnet4Flag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET4.getName());
|
||||
if(testnet4Flag.exists()) {
|
||||
Network.set(Network.TESTNET4);
|
||||
}
|
||||
|
||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||
if(signetFlag.exists()) {
|
||||
Network.set(Network.SIGNET);
|
||||
|
||||
@@ -5,17 +5,13 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
|
||||
|
||||
@@ -46,12 +42,14 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
standardAccountCombo = new ComboBox<>();
|
||||
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
List<Integer> existingIndexes = new ArrayList<>();
|
||||
Set<Integer> existingIndexes = new LinkedHashSet<>();
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
existingIndexes.add(masterWallet.getAccountIndex());
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
existingIndexes.add(childWallet.getAccountIndex());
|
||||
Optional<StandardAccount> optStdAcc = Arrays.stream(StandardAccount.values()).filter(stdacc -> stdacc.getName().equals(childWallet.getName())).findFirst();
|
||||
optStdAcc.ifPresent(standardAccount -> existingIndexes.add(standardAccount.getAccountNumber()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,15 +60,14 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
}
|
||||
}
|
||||
|
||||
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
if(AppServices.isWhirlpoolCompatible(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
availableAccounts.add(WHIRLPOOL_PREMIX);
|
||||
} else if(WhirlpoolServices.canWatchPostmix(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
|
||||
} else if(AppServices.isWhirlpoolPostmixCompatible(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
|
||||
availableAccounts.add(WHIRLPOOL_POSTMIX);
|
||||
}
|
||||
|
||||
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
||||
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
|
||||
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
if(!availableAccounts.isEmpty() && (masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||
dialogPane.getButtonTypes().add(discoverButtonType);
|
||||
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
||||
|
||||
@@ -45,6 +45,7 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
|
||||
setTooltip(tooltip);
|
||||
getStyleClass().add("address-cell");
|
||||
|
||||
if(addressStatus.isDustAttack()) {
|
||||
setGraphic(getDustAttackHyperlink(utxoEntry));
|
||||
|
||||
@@ -77,7 +77,7 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
getColumns().forEach(col -> col.setContextMenu(contextMenu));
|
||||
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
setupColumnWidths();
|
||||
|
||||
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
|
||||
getSortOrder().add(addressCol);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||
|
||||
public class BitBoxPairingDialog extends Alert {
|
||||
public BitBoxPairingDialog(String code) {
|
||||
super(AlertType.INFORMATION);
|
||||
initOwner(getActiveWindow());
|
||||
setStageIcon(getDialogPane().getScene().getWindow());
|
||||
getDialogPane().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
setTitle("Confirm BitBox02 Pairing");
|
||||
setHeaderText(getTitle());
|
||||
VBox vBox = new VBox(20);
|
||||
vBox.setAlignment(Pos.CENTER);
|
||||
vBox.setPadding(new Insets(10, 20, 10, 20));
|
||||
Label instructions = new Label("Confirm the following code is shown on BitBox02");
|
||||
Label codeLabel = new Label(code);
|
||||
codeLabel.getStyleClass().add("fixed-width");
|
||||
vBox.getChildren().addAll(instructions, codeLabel);
|
||||
getDialogPane().setContent(vBox);
|
||||
moveToActiveWindowScreen(this);
|
||||
getDialogPane().getButtonTypes().clear();
|
||||
getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
@@ -16,7 +17,6 @@ import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
@@ -32,7 +32,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
tooltip.setShowDelay(Duration.millis(500));
|
||||
contextMenu = new CoinContextMenu();
|
||||
getStyleClass().add("coin-cell");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
getStyleClass().add("number-field");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.text.ParseException;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CoinTextFormatter extends TextFormatter<String> {
|
||||
@@ -51,8 +51,14 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||
commasRemoved = newText.length() - noFractionCommaText.length();
|
||||
}
|
||||
|
||||
if(!coinValidation.matcher(noFractionCommaText).matches()) {
|
||||
return null;
|
||||
Matcher matcher = coinValidation.matcher(noFractionCommaText);
|
||||
if(!matcher.matches()) {
|
||||
matcher.reset();
|
||||
if(matcher.find()) {
|
||||
noFractionCommaText = matcher.group();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if(unitFormat.getGroupingSeparator().equals(change.getText())) {
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.SortDirection;
|
||||
import com.sparrowwallet.drongo.wallet.TableType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTable;
|
||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletTableChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
@@ -13,24 +17,38 @@ import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
private TableType tableType;
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
private UnitFormat unitFormat;
|
||||
private CurrencyRate currencyRate;
|
||||
protected static final double STANDARD_WIDTH = 100.0;
|
||||
|
||||
private final PublishSubject<WalletTableChangedEvent> walletTableSubject = PublishSubject.create();
|
||||
private final Observable<WalletTableChangedEvent> walletTableEvents = walletTableSubject.debounce(1, TimeUnit.SECONDS);
|
||||
|
||||
public TableType getTableType() {
|
||||
return tableType;
|
||||
}
|
||||
|
||||
public void setTableType(TableType tableType) {
|
||||
this.tableType = tableType;
|
||||
}
|
||||
|
||||
public BitcoinUnit getBitcoinUnit() {
|
||||
return bitcoinUnit;
|
||||
@@ -136,11 +154,100 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
public void setSortColumn(int columnIndex, TreeTableColumn.SortType sortType) {
|
||||
if(columnIndex >= 0 && columnIndex < getColumns().size() && getSortOrder().isEmpty() && !getRoot().getChildren().isEmpty()) {
|
||||
TreeTableColumn<Entry, ?> column = getColumns().get(columnIndex);
|
||||
column.setSortType(sortType == null ? TreeTableColumn.SortType.DESCENDING : sortType);
|
||||
protected void setupColumnSort(int defaultColumnIndex, TreeTableColumn.SortType defaultSortType) {
|
||||
WalletTable.Sort columnSort = getSavedColumnSort();
|
||||
if(columnSort == null) {
|
||||
columnSort = new WalletTable.Sort(defaultColumnIndex, getSortDirection(defaultSortType));
|
||||
}
|
||||
|
||||
setSortColumn(columnSort);
|
||||
|
||||
getSortOrder().addListener((ListChangeListener<? super TreeTableColumn<Entry, ?>>) c -> {
|
||||
if(c.next()) {
|
||||
walletTableChanged();
|
||||
}
|
||||
});
|
||||
for(TreeTableColumn<Entry, ?> column : getColumns()) {
|
||||
column.sortTypeProperty().addListener((_, _, _) -> walletTableChanged());
|
||||
}
|
||||
}
|
||||
|
||||
protected void resetSortColumn() {
|
||||
setSortColumn(getColumnSort());
|
||||
}
|
||||
|
||||
protected void setSortColumn(WalletTable.Sort sort) {
|
||||
if(sort.sortColumn() >= 0 && sort.sortColumn() < getColumns().size() && getSortOrder().isEmpty() && !getRoot().getChildren().isEmpty()) {
|
||||
TreeTableColumn<Entry, ?> column = getColumns().get(sort.sortColumn());
|
||||
column.setSortType(sort.sortDirection() == SortDirection.DESCENDING ? TreeTableColumn.SortType.DESCENDING : TreeTableColumn.SortType.ASCENDING);
|
||||
getSortOrder().add(column);
|
||||
}
|
||||
}
|
||||
|
||||
private WalletTable.Sort getColumnSort() {
|
||||
if(getSortOrder().isEmpty() || !getColumns().contains(getSortOrder().getFirst())) {
|
||||
return new WalletTable.Sort(tableType == TableType.UTXOS ? getColumns().size() - 1 : 0, SortDirection.DESCENDING);
|
||||
}
|
||||
|
||||
return new WalletTable.Sort(getColumns().indexOf(getSortOrder().getFirst()), getSortDirection(getSortOrder().getFirst().getSortType()));
|
||||
}
|
||||
|
||||
private SortDirection getSortDirection(TreeTableColumn.SortType sortType) {
|
||||
return sortType == TreeTableColumn.SortType.ASCENDING ? SortDirection.ASCENDING : SortDirection.DESCENDING;
|
||||
}
|
||||
|
||||
private WalletTable.Sort getSavedColumnSort() {
|
||||
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
|
||||
Wallet wallet = getRoot().getValue().getWallet();
|
||||
WalletTable walletTable = wallet.getWalletTable(tableType);
|
||||
if(walletTable != null) {
|
||||
return walletTable.getSort();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void setupColumnWidths() {
|
||||
Double[] savedWidths = getSavedColumnWidths();
|
||||
for(int i = 0; i < getColumns().size(); i++) {
|
||||
TreeTableColumn<Entry, ?> column = getColumns().get(i);
|
||||
column.setPrefWidth(savedWidths != null && getColumns().size() == savedWidths.length ? savedWidths[i] : STANDARD_WIDTH);
|
||||
}
|
||||
|
||||
//TODO: Replace with TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN when JavaFX 20+ has headless support
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
|
||||
getColumns().getLast().widthProperty().addListener((_, _, _) -> walletTableChanged());
|
||||
|
||||
//Ignore initial resizes during layout
|
||||
walletTableEvents.skip(3, TimeUnit.SECONDS).subscribe(event -> {
|
||||
event.getWallet().getWalletTables().put(event.getTableType(), event.getWalletTable());
|
||||
EventManager.get().post(event);
|
||||
});
|
||||
}
|
||||
|
||||
private void walletTableChanged() {
|
||||
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
|
||||
WalletTable walletTable = new WalletTable(tableType, getColumnWidths(), getColumnSort());
|
||||
walletTableSubject.onNext(new WalletTableChangedEvent(getRoot().getValue().getWallet(), walletTable));
|
||||
}
|
||||
}
|
||||
|
||||
private Double[] getColumnWidths() {
|
||||
return getColumns().stream().map(TableColumnBase::getWidth).toArray(Double[]::new);
|
||||
}
|
||||
|
||||
private Double[] getSavedColumnWidths() {
|
||||
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
|
||||
Wallet wallet = getRoot().getValue().getWallet();
|
||||
WalletTable walletTable = wallet.getWalletTable(tableType);
|
||||
if(walletTable != null) {
|
||||
return walletTable.getWidths();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import javafx.beans.value.ChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
@@ -52,6 +53,7 @@ public class CopyableTextField extends CustomTextField {
|
||||
selectedTextProperty().removeListener(selectionListener);
|
||||
}
|
||||
});
|
||||
setContextMenu(new ContextMenu());
|
||||
}
|
||||
|
||||
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
|
||||
|
||||
@@ -3,13 +3,14 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.PdfUtils;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Button;
|
||||
|
||||
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
|
||||
super(ur);
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, boolean selectBbqrButton) {
|
||||
super(ur, bbqr, false, false, selectBbqrButton);
|
||||
|
||||
DialogPane dialogPane = getDialogPane();
|
||||
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
|
||||
@@ -19,7 +20,7 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
pdfButton.setGraphicTextGap(5);
|
||||
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
||||
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur);
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, isUseBbqrEncoding() ? bbqr : null);
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,11 +16,8 @@ import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.CardApi;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
@@ -778,10 +775,12 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
signButton.setDisable(false);
|
||||
}
|
||||
} else {
|
||||
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
|
||||
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt,
|
||||
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
|
||||
signPSBTService.setOnSucceeded(workerStateEvent -> {
|
||||
PSBT signedPsbt = signPSBTService.getValue();
|
||||
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
|
||||
updateDeviceRegistrations(signPSBTService.getNewDeviceRegistrations());
|
||||
});
|
||||
signPSBTService.setOnFailed(workerStateEvent -> {
|
||||
setError("Signing Error", signPSBTService.getException().getMessage());
|
||||
@@ -820,10 +819,12 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void displayAddress() {
|
||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
|
||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor,
|
||||
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
|
||||
displayAddressService.setOnSucceeded(successEvent -> {
|
||||
String address = displayAddressService.getValue();
|
||||
EventManager.get().post(new AddressDisplayedEvent(address));
|
||||
updateDeviceRegistrations(displayAddressService.getNewDeviceRegistrations());
|
||||
});
|
||||
displayAddressService.setOnFailed(failedEvent -> {
|
||||
setError("Could not display address", displayAddressService.getException().getMessage());
|
||||
@@ -833,6 +834,26 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
displayAddressService.start();
|
||||
}
|
||||
|
||||
private byte[] getDeviceRegistration() {
|
||||
Optional<Keystore> optKeystore = wallet.getKeystores().stream()
|
||||
.filter(keystore -> keystore.getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint()) && keystore.getDeviceRegistration() != null).findFirst();
|
||||
return optKeystore.map(Keystore::getDeviceRegistration).orElse(null);
|
||||
}
|
||||
|
||||
private void updateDeviceRegistrations(Set<byte[]> newDeviceRegistrations) {
|
||||
if(!newDeviceRegistrations.isEmpty()) {
|
||||
List<Keystore> registrationKeystores = getDeviceRegistrationKeystores();
|
||||
if(!registrationKeystores.isEmpty()) {
|
||||
registrationKeystores.forEach(keystore -> keystore.setDeviceRegistration(newDeviceRegistrations.iterator().next()));
|
||||
EventManager.get().post(new KeystoreDeviceRegistrationsChangedEvent(wallet, registrationKeystores));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Keystore> getDeviceRegistrationKeystores() {
|
||||
return wallet.getKeystores().stream().filter(keystore -> keystore.getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint())).toList();
|
||||
}
|
||||
|
||||
private void signMessage() {
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.pgp.PGPKeySource;
|
||||
import com.sparrowwallet.drongo.pgp.PGPUtils;
|
||||
@@ -24,7 +25,6 @@ import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
@@ -500,9 +500,9 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
private List<String> getReleaseFileExtensions() {
|
||||
Platform platform = Platform.getCurrent();
|
||||
switch(platform) {
|
||||
case OSX -> {
|
||||
OsType osType = OsType.getCurrent();
|
||||
switch(osType) {
|
||||
case MACOS -> {
|
||||
return MACOS_RELEASE_EXTENSIONS;
|
||||
}
|
||||
case WINDOWS -> {
|
||||
@@ -515,10 +515,10 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
private String getReleaseFileExample(String version) {
|
||||
Platform platform = Platform.getCurrent();
|
||||
OsType osType = OsType.getCurrent();
|
||||
String arch = System.getProperty("os.arch");
|
||||
switch(platform) {
|
||||
case OSX -> {
|
||||
switch(osType) {
|
||||
case MACOS -> {
|
||||
return "Sparrow-" + version + "-" + arch;
|
||||
}
|
||||
case WINDOWS -> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
@@ -459,7 +460,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
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);
|
||||
long blocksFromTip = (long)Math.ceil((double)vSizefromTip / Transaction.MAX_BLOCK_SIZE_VBYTES);
|
||||
|
||||
String amount = vSizefromTip + " vB";
|
||||
if(vSizefromTip > 1000 * 1000) {
|
||||
@@ -606,7 +607,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionContextMenu extends ContextMenu {
|
||||
protected static class TransactionContextMenu extends ContextMenu {
|
||||
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
@@ -805,18 +806,17 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
cell.getStyleClass().remove("utxo-row");
|
||||
cell.getStyleClass().remove("unconfirmed-row");
|
||||
cell.getStyleClass().remove("summary-row");
|
||||
cell.getStyleClass().remove("address-cell");
|
||||
boolean addressCell = cell.getStyleClass().remove("address-cell");
|
||||
cell.getStyleClass().remove("hashindex-row");
|
||||
cell.getStyleClass().remove("confirming");
|
||||
cell.getStyleClass().remove("negative-amount");
|
||||
cell.getStyleClass().remove("spent");
|
||||
cell.getStyleClass().remove("unspendable");
|
||||
cell.getStyleClass().remove("number-field");
|
||||
|
||||
if(entry != null) {
|
||||
if(entry instanceof TransactionEntry) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
cell.getStyleClass().add("transaction-row");
|
||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||
|
||||
if(cell instanceof ConfirmationsListener confirmationsListener) {
|
||||
if(transactionEntry.isConfirming()) {
|
||||
cell.getStyleClass().add("confirming");
|
||||
@@ -825,23 +825,27 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
confirmationsListener.getConfirmationsProperty().unbind();
|
||||
}
|
||||
}
|
||||
if(OsType.getCurrent() == OsType.MACOS && transactionEntry.getBlockTransaction().getHeight() > 0 && !cell.getStyleClass().contains("label-cell")) {
|
||||
cell.getStyleClass().add("number-field");
|
||||
}
|
||||
} else if(entry instanceof NodeEntry) {
|
||||
cell.getStyleClass().add("node-row");
|
||||
} else if(entry instanceof UtxoEntry) {
|
||||
} else if(entry instanceof UtxoEntry utxoEntry) {
|
||||
cell.getStyleClass().add("utxo-row");
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
if(!utxoEntry.isSpendable()) {
|
||||
cell.getStyleClass().add("unspendable");
|
||||
}
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
if(OsType.getCurrent() == OsType.MACOS && utxoEntry.getHashIndex().getHeight() > 0 && !addressCell && !cell.getStyleClass().contains("label-cell")) {
|
||||
cell.getStyleClass().add("number-field");
|
||||
}
|
||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
cell.getStyleClass().add("hashindex-row");
|
||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||
if(hashIndexEntry.isSpent()) {
|
||||
cell.getStyleClass().add("spent");
|
||||
}
|
||||
} else if(entry instanceof WalletSummaryDialog.UnconfirmedEntry) {
|
||||
cell.getStyleClass().add("unconfirmed-row");
|
||||
} else if(entry instanceof WalletSummaryDialog.SummaryEntry) {
|
||||
} else if(entry instanceof WalletSummaryDialog.SummaryEntry || entry instanceof WalletSummaryDialog.AllSummaryEntry) {
|
||||
cell.getStyleClass().add("summary-row");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import java.util.stream.Collectors;
|
||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||
|
||||
public class FeeRangeSlider extends Slider {
|
||||
private static final double FEE_RATE_SCROLL_INCREMENT = 0.01;
|
||||
|
||||
public FeeRangeSlider() {
|
||||
super(0, FEE_RATES_RANGE.size() - 1, 0);
|
||||
setMajorTickUnit(1);
|
||||
@@ -45,6 +47,18 @@ public class FeeRangeSlider extends Slider {
|
||||
updateMaxFeeRange(newValue.doubleValue());
|
||||
}
|
||||
});
|
||||
|
||||
setOnScroll(event -> {
|
||||
if(event.getDeltaY() != 0) {
|
||||
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
|
||||
if(newFeeRate < LONG_FEE_RATES_RANGE.get(0)) {
|
||||
newFeeRate = LONG_FEE_RATES_RANGE.get(0);
|
||||
} else if(newFeeRate > LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1)) {
|
||||
newFeeRate = LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1);
|
||||
}
|
||||
setFeeRate(newFeeRate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public double getFeeRate() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
@@ -10,7 +11,6 @@ import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Currency;
|
||||
@@ -24,7 +24,7 @@ public class FiatCell extends TreeTableCell<Entry, Number> {
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new FiatContextMenu();
|
||||
getStyleClass().add("coin-cell");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
getStyleClass().add("number-field");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
@@ -24,9 +25,7 @@ import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -104,7 +103,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " File");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", Platform.getCurrent().equals(Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("JSON", "*.json"),
|
||||
new FileChooser.ExtensionFilter("TXT", "*.txt")
|
||||
);
|
||||
@@ -240,6 +239,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
javafx.application.Platform.runLater(passwordField::requestFocus);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
super(importer, importer.getName(), "Key import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
|
||||
@@ -171,11 +171,18 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
} else if(exporter instanceof Bip129) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
|
||||
} else if(exporter instanceof Descriptor) {
|
||||
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
|
||||
boolean selectBbqrOption = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr());
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR());
|
||||
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, selectBbqrOption);
|
||||
} else if(exporter.getClass().equals(ColdcardMultisig.class)) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, true);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -38,6 +39,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private final KeystoreFileImport importer;
|
||||
private String fileName;
|
||||
private byte[] fileBytes;
|
||||
private String password;
|
||||
|
||||
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
@@ -46,6 +48,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
|
||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||
this.fileName = fileName;
|
||||
this.password = password;
|
||||
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||
if(wallets != null && !wallets.isEmpty()) {
|
||||
@@ -83,7 +86,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
||||
Keystore keystore = importer.getKeystore(scriptType, bais, "");
|
||||
Keystore keystore = importer.getKeystore(scriptType, bais, password);
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(Files.getNameWithoutExtension(fileName));
|
||||
@@ -151,6 +154,8 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
Platform.runLater(scriptTypeComboBox::requestFocus);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.drongo.wallet.Persistable;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.event.Event;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTreeTableCell;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.DataFormat;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.converter.DefaultStringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -47,6 +51,20 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||
public void commitEdit(String label) {
|
||||
if(label != null) {
|
||||
label = label.trim();
|
||||
if(label.length() > Persistable.MAX_LABEL_LENGTH) {
|
||||
label = label.substring(0, Persistable.MAX_LABEL_LENGTH);
|
||||
Platform.runLater(() -> {
|
||||
Point2D p = this.localToScene(0.0, 0.0);
|
||||
final Tooltip truncateTooltip = new Tooltip();
|
||||
truncateTooltip.setText("Labels are truncated at " + Persistable.MAX_LABEL_LENGTH + " characters");
|
||||
truncateTooltip.setAutoHide(true);
|
||||
truncateTooltip.show(this, p.getX() + this.getScene().getX() + this.getScene().getWindow().getX() + this.getHeight(),
|
||||
p.getY() + this.getScene().getY() + this.getScene().getWindow().getY() + this.getHeight());
|
||||
PauseTransition pt = new PauseTransition(Duration.millis(2000));
|
||||
pt.setOnFinished(_ -> truncateTooltip.hide());
|
||||
pt.play();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// This block is necessary to support commit on losing focus, because
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
@@ -57,7 +58,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
stage.setResizable(false);
|
||||
|
||||
StackPane scenePane = new StackPane();
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
if(OsType.getCurrent() == OsType.WINDOWS) {
|
||||
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
@@ -17,10 +18,13 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
@@ -32,17 +36,21 @@ import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
|
||||
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
|
||||
|
||||
private static final Pattern signedMessagePattern = Pattern.compile("-----BEGIN BITCOIN SIGNED MESSAGE-----\\r?\\n(.*)\\r?\\n-----BEGIN BITCOIN SIGNATURE-----\\r?\\n(.*)\\r?\\n(.*)\\r?\\n-----END BITCOIN SIGNATURE-----\r?\n?");
|
||||
|
||||
private final TextField address;
|
||||
private final TextArea message;
|
||||
private final TextArea signature;
|
||||
@@ -104,7 +112,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
this.wallet = wallet;
|
||||
this.walletNode = walletNode;
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
final DialogPane dialogPane = new MessageSignDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
@@ -199,13 +208,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
} else {
|
||||
dialogPane.getButtonTypes().addAll(showQrButtonType, signButtonType, verifyButtonType, doneButtonType);
|
||||
|
||||
Button showQrButton = (Button) dialogPane.lookupButton(showQrButtonType);
|
||||
showQrButton.setDisable(wallet == null);
|
||||
showQrButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
|
||||
showQrButton.setGraphicTextGap(5);
|
||||
showQrButton.setOnAction(event -> {
|
||||
showQr();
|
||||
});
|
||||
Node showQrButton = dialogPane.lookupButton(showQrButtonType);
|
||||
|
||||
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
|
||||
signButton.setDisable(!canSign);
|
||||
@@ -267,7 +270,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
setResultConverter(dialogButton -> dialogButton == showQrButtonType || dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if(address.getText().isEmpty()) {
|
||||
@@ -495,6 +498,82 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private void exportFile() {
|
||||
if(walletNode == null) {
|
||||
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
|
||||
return;
|
||||
}
|
||||
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
joiner.add(message.getText().trim().replaceAll("\r*\n*", ""));
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
|
||||
joiner.add(KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), true));
|
||||
joiner.add(walletNode.getWallet().getScriptType().toString());
|
||||
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save Text File");
|
||||
fileChooser.setInitialFileName("signmessage.txt");
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
if(!file.getName().toLowerCase(Locale.ROOT).endsWith(".txt")) {
|
||||
file = new File(file.getAbsolutePath() + ".txt");
|
||||
}
|
||||
|
||||
try(BufferedWriter writer = new BufferedWriter(new FileWriter(file, StandardCharsets.UTF_8))) {
|
||||
writer.write(joiner.toString());
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving signing message", e);
|
||||
AppServices.showErrorDialog("Error saving signing message", "Cannot write to " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void importFile() {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Signed Text File");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("Text Files", "*.txt")
|
||||
);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
|
||||
if(file != null) {
|
||||
try {
|
||||
String content = Files.readString(file.toPath(), StandardCharsets.UTF_8);
|
||||
Matcher matcher = signedMessagePattern.matcher(content);
|
||||
if(matcher.matches()) {
|
||||
String signedMessage = matcher.group(1);
|
||||
String signedAddress = matcher.group(2);
|
||||
String signedSignature = matcher.group(3);
|
||||
|
||||
if(!message.getText().isEmpty() && !signedMessage.trim().equals(message.getText().trim().replaceAll("\r*\n*", ""))) {
|
||||
AppServices.showErrorDialog("Incorrect Message", "The file contained a different message of:\n\n" + signedMessage);
|
||||
return;
|
||||
} else if(!signedAddress.trim().equals(address.getText().trim())) {
|
||||
AppServices.showErrorDialog("Incorrect Address", "The file contained a different address of:\n\n" + signedAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
message.setText(signedMessage);
|
||||
signature.setText(signedSignature);
|
||||
} else {
|
||||
signature.setText(content);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Error loading signed message", e);
|
||||
AppServices.showErrorDialog("Error loading signed message", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Glyph getSignGlyph() {
|
||||
if(wallet != null) {
|
||||
if(wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
@@ -539,4 +618,37 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
decryptWalletService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageSignDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
SplitMenuButton signByButton = new SplitMenuButton();
|
||||
signByButton.setText("Sign by QR");
|
||||
signByButton.setDisable(wallet == null);
|
||||
signByButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
|
||||
signByButton.setGraphicTextGap(5);
|
||||
signByButton.setOnAction(event -> {
|
||||
showQr();
|
||||
});
|
||||
MenuItem exportFile = new MenuItem("Sign by File...");
|
||||
exportFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_EXPORT)));
|
||||
exportFile.setOnAction(event -> {
|
||||
exportFile();
|
||||
});
|
||||
MenuItem importFile = new MenuItem("Load Signed File...");
|
||||
importFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_IMPORT)));
|
||||
importFile.setOnAction(event -> {
|
||||
importFile();
|
||||
});
|
||||
signByButton.getItems().addAll(exportFile, importFile);
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(signByButton, buttonData);
|
||||
|
||||
return signByButton;
|
||||
}
|
||||
|
||||
return super.createButton(buttonType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.mix.listener.MixStep;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
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;
|
||||
|
||||
public MixStatusCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -41,162 +25,9 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(Integer.toString(mixStatus.getMixesDone()));
|
||||
if(mixStatus.getNextMixUtxo() == null) {
|
||||
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
}
|
||||
|
||||
if(mixStatus.getNextMixUtxo() != null) {
|
||||
setMixSuccess(mixStatus.getNextMixUtxo());
|
||||
} else if(mixStatus.getMixFailReason() != null) {
|
||||
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
|
||||
} else if(mixStatus.getMixProgress() != null) {
|
||||
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setMixSuccess(Utxo nextMixUtxo) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(-1);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
|
||||
setTooltip(tt);
|
||||
}
|
||||
|
||||
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
|
||||
if(mixFailReason.isError()) {
|
||||
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
|
||||
if(elapsed >= ERROR_DISPLAY_MILLIS) {
|
||||
//Old error, don't set again.
|
||||
return;
|
||||
}
|
||||
|
||||
Glyph failGlyph = getFailGlyph();
|
||||
setGraphic(failGlyph);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
|
||||
"\nMix failures are generally caused by peers disconnecting during a mix." +
|
||||
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
|
||||
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
|
||||
setTooltip(tt);
|
||||
|
||||
Duration fadeDuration = Duration.millis(ERROR_DISPLAY_MILLIS - elapsed);
|
||||
double fadeFromValue = 1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS);
|
||||
Timeline timeline = AnimationUtil.getSlowFadeOut(failGlyph, fadeDuration, fadeFromValue, 10);
|
||||
timeline.setOnFinished(event -> {
|
||||
setTooltip(null);
|
||||
});
|
||||
timeline.play();
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPlatformSleepConfig() {
|
||||
Platform platform = Platform.getCurrent();
|
||||
if(platform == Platform.OSX) {
|
||||
return "OSX System Preferences";
|
||||
} else if(platform == Platform.WINDOWS) {
|
||||
return "Windows Control Panel";
|
||||
}
|
||||
|
||||
return "system power settings";
|
||||
}
|
||||
|
||||
private void setMixProgress(UtxoEntry utxoEntry, MixProgress mixProgress) {
|
||||
if(mixProgress.getMixStep() != MixStep.FAIL) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
String status = mixProgress.getMixStep().getMessage().replaceAll("_", " ");
|
||||
status = status.substring(0, 1).toUpperCase(Locale.ROOT) + status.substring(1).toLowerCase(Locale.ROOT);
|
||||
tt.setText(status);
|
||||
setTooltip(tt);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private ProgressIndicator getProgressIndicator() {
|
||||
ProgressIndicator progressIndicator;
|
||||
if(getGraphic() instanceof ProgressIndicator) {
|
||||
progressIndicator = (ProgressIndicator)getGraphic();
|
||||
} else {
|
||||
progressIndicator = new ProgressBar();
|
||||
}
|
||||
|
||||
return progressIndicator;
|
||||
}
|
||||
|
||||
private static Glyph getMixGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getStopGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFailGlyph() {
|
||||
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
failGlyph.getStyleClass().add("fail-warning");
|
||||
failGlyph.setFontSize(12);
|
||||
return failGlyph;
|
||||
}
|
||||
|
||||
private static class MixStatusContextMenu extends ContextMenu {
|
||||
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
|
||||
Whirlpool pool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(isMixing) {
|
||||
MenuItem mixStop = new MenuItem("Stop Mixing");
|
||||
if(pool != null) {
|
||||
mixStop.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
mixStop.setGraphic(getStopGlyph());
|
||||
mixStop.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mixStop(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixStop);
|
||||
} else {
|
||||
MenuItem mixNow = new MenuItem("Mix Now");
|
||||
if(pool != null) {
|
||||
mixNow.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
|
||||
mixNow.setGraphic(getMixGlyph());
|
||||
mixNow.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mix(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@@ -256,7 +257,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open PDF");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("PDF", "*.pdf")
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
@@ -15,10 +16,13 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
private final DeterministicSeed.Type type;
|
||||
|
||||
public MnemonicKeystoreDisplayPane(Keystore keystore) {
|
||||
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();
|
||||
this.type = keystore.getSeed().getType();
|
||||
|
||||
showWordList(keystore.getSeed());
|
||||
}
|
||||
@@ -29,7 +33,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords / 3);
|
||||
wordsPane.setPrefRows(Math.ceilDiv(numWords, 3));
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
@@ -43,7 +47,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider()));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
@@ -57,4 +61,9 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
stackPane.getChildren().add(vBox);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WordlistProvider getWordlistProvider() {
|
||||
return getWordListProvider(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.Slip39MnemonicCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
@@ -111,23 +113,9 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
wordEntry.getEditor().setText(words.get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
} else {
|
||||
ScheduledService<Void> service = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
service.setDelay(Duration.millis(500));
|
||||
service.setOnSucceeded(event1 -> {
|
||||
service.cancel();
|
||||
AppServices.runAfterDelay(500, () -> {
|
||||
Platform.runLater(() -> wordEntry.getEditor().requestFocus());
|
||||
});
|
||||
service.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +141,10 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
protected void showWordList(DeterministicSeed seed) {
|
||||
List<String> words = seed.getMnemonicCode();
|
||||
showWordList(words);
|
||||
}
|
||||
|
||||
protected void showWordList(List<String> words) {
|
||||
setContent(getMnemonicWordsEntry(words.size(), true, true));
|
||||
setExpanded(true);
|
||||
|
||||
@@ -175,7 +167,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords/3);
|
||||
wordsPane.setPrefRows(Math.ceilDiv(numWords, 3));
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
@@ -189,7 +181,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider()));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
@@ -215,7 +207,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
buttonPane.getChildren().add(leftBox);
|
||||
AnchorPane.setLeftAnchor(leftBox, 0.0);
|
||||
|
||||
validLabel = new Label("Valid checksum", getValidGlyph());
|
||||
validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph());
|
||||
validLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
validLabel.setGraphicTextGap(5.0);
|
||||
validLabel.managedProperty().bind(validLabel.visibleProperty());
|
||||
@@ -224,7 +216,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
AnchorPane.setTopAnchor(validLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(validLabel, 0.0);
|
||||
|
||||
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
|
||||
invalidLabel = new Label("Invalid checksum", GlyphUtils.getInvalidGlyph());
|
||||
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
invalidLabel.setGraphicTextGap(5.0);
|
||||
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
|
||||
@@ -242,7 +234,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
empty = false;
|
||||
}
|
||||
|
||||
if(!WordEntry.isValid(word)) {
|
||||
if(!getWordlistProvider().isValid(word)) {
|
||||
validWords = false;
|
||||
}
|
||||
}
|
||||
@@ -278,13 +270,20 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
//nothing by default
|
||||
}
|
||||
|
||||
protected WordlistProvider getWordlistProvider() {
|
||||
return getWordListProvider(DeterministicSeed.Type.BIP39);
|
||||
}
|
||||
|
||||
protected WordlistProvider getWordListProvider(DeterministicSeed.Type type) {
|
||||
return type == DeterministicSeed.Type.SLIP39 ? new Slip39WordlistProvider() : new Bip39WordlistProvider();
|
||||
}
|
||||
|
||||
protected static class WordEntry extends HBox {
|
||||
private static List<String> wordList;
|
||||
private final TextField wordField;
|
||||
private WordEntry nextEntry;
|
||||
private TextField nextField;
|
||||
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList, WordlistProvider wordlistProvider) {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
@@ -302,7 +301,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
for(int i = 0; i < words.length; i++) {
|
||||
String word = words[i];
|
||||
if(entry.nextField != null) {
|
||||
if(i == words.length - 2 && isValid(word)) {
|
||||
if(i == words.length - 2 && wordlistProvider.isValid(word)) {
|
||||
label.requestFocus();
|
||||
} else {
|
||||
entry.nextField.requestFocus();
|
||||
@@ -335,8 +334,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
});
|
||||
wordField.setTextFormatter(formatter);
|
||||
|
||||
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList, wordNumber, wordEntryList));
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordlistProvider, wordNumber, wordEntryList));
|
||||
autoCompletionBinding.setDelay(50);
|
||||
autoCompletionBinding.setOnAutoCompleted(event -> {
|
||||
if(nextField != null) {
|
||||
@@ -357,7 +355,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(wordField, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordList.contains(newValue))
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordlistProvider.isValid(newValue))
|
||||
));
|
||||
|
||||
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
@@ -378,28 +376,24 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
public void setNextField(TextField field) {
|
||||
this.nextField = field;
|
||||
}
|
||||
|
||||
public static boolean isValid(String word) {
|
||||
return wordList.contains(word);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
|
||||
private final List<String> wordList;
|
||||
private final WordlistProvider wordlistProvider;
|
||||
private final int wordNumber;
|
||||
private final ObservableList<String> wordEntryList;
|
||||
|
||||
public WordlistSuggestionProvider(List<String> wordList, int wordNumber, ObservableList<String> wordEntryList) {
|
||||
this.wordList = wordList;
|
||||
public WordlistSuggestionProvider(WordlistProvider wordlistProvider, int wordNumber, ObservableList<String> wordEntryList) {
|
||||
this.wordlistProvider = wordlistProvider;
|
||||
this.wordNumber = wordNumber;
|
||||
this.wordEntryList = wordEntryList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
|
||||
if(wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
|
||||
if(wordlistProvider.supportsPossibleLastWords() && wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
|
||||
try {
|
||||
List<String> possibleLastWords = Bip39MnemonicCode.INSTANCE.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
|
||||
List<String> possibleLastWords = wordlistProvider.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText()));
|
||||
}
|
||||
@@ -412,7 +406,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
for(String word : wordList) {
|
||||
for(String word : wordlistProvider.getWordlist()) {
|
||||
if(word.startsWith(request.getUserText())) {
|
||||
suggestions.add(word);
|
||||
}
|
||||
@@ -424,7 +418,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
private boolean allPreviousWordsValid() {
|
||||
for(int i = 0; i < wordEntryList.size() - 1; i++) {
|
||||
if(!WordEntry.isValid(wordEntryList.get(i))) {
|
||||
if(!wordlistProvider.isValid(wordEntryList.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -485,17 +479,53 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
public static Glyph getValidGlyph() {
|
||||
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||
validGlyph.getStyleClass().add("success");
|
||||
validGlyph.setFontSize(12);
|
||||
return validGlyph;
|
||||
protected interface WordlistProvider {
|
||||
List<String> getWordlist();
|
||||
boolean isValid(String word);
|
||||
boolean supportsPossibleLastWords();
|
||||
List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException;
|
||||
}
|
||||
|
||||
public static Glyph getInvalidGlyph() {
|
||||
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
invalidGlyph.getStyleClass().add("failure");
|
||||
invalidGlyph.setFontSize(12);
|
||||
return invalidGlyph;
|
||||
private static class Bip39WordlistProvider implements WordlistProvider {
|
||||
@Override
|
||||
public List<String> getWordlist() {
|
||||
return Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
}
|
||||
|
||||
public boolean isValid(String word) {
|
||||
return getWordlist().contains(word);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPossibleLastWords() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException {
|
||||
return Bip39MnemonicCode.INSTANCE.getPossibleLastWords(previousWords);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Slip39WordlistProvider implements WordlistProvider {
|
||||
@Override
|
||||
public List<String> getWordlist() {
|
||||
return Slip39MnemonicCode.INSTANCE.getWordList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String word) {
|
||||
return getWordlist().contains(word);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPossibleLastWords() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleLastWords(List<String> previousWords) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.Share;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreMnemonicShareImport;
|
||||
import com.sparrowwallet.sparrow.io.Slip39;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected final Wallet wallet;
|
||||
private final KeystoreMnemonicShareImport importer;
|
||||
private final KeyDerivation defaultDerivation;
|
||||
private final List<List<String>> mnemonicShares = new ArrayList<>();
|
||||
|
||||
private SplitMenuButton importButton;
|
||||
|
||||
private Button calculateButton;
|
||||
private Button backButton;
|
||||
private Button nextButton;
|
||||
private int currentShare;
|
||||
|
||||
public MnemonicShareKeystoreImportPane(Wallet wallet, KeystoreMnemonicShareImport importer, KeyDerivation defaultDerivation) {
|
||||
super(importer.getName(), "Enter seed share", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
|
||||
createImportButton();
|
||||
buttonBox.getChildren().add(importButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
createEnterMnemonicButton();
|
||||
return enterMnemonicButton;
|
||||
}
|
||||
|
||||
private void createEnterMnemonicButton() {
|
||||
enterMnemonicButton = new SplitMenuButton();
|
||||
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
enterMnemonicButton.setText("Use 20 Words");
|
||||
defaultWordSizeProperty = new SimpleIntegerProperty(20);
|
||||
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
|
||||
enterMnemonicButton.setText("Use " + newValue + " Words");
|
||||
});
|
||||
enterMnemonicButton.setOnAction(event -> {
|
||||
resetShares();
|
||||
enterMnemonic(defaultWordSizeProperty.get());
|
||||
});
|
||||
int[] numberWords = new int[] {20, 33};
|
||||
for(int i = 0; i < numberWords.length; i++) {
|
||||
MenuItem item = new MenuItem("Use " + numberWords[i] + " Words");
|
||||
final int words = numberWords[i];
|
||||
item.setOnAction(event -> {
|
||||
resetShares();
|
||||
defaultWordSizeProperty.set(words);
|
||||
enterMnemonic(words);
|
||||
});
|
||||
enterMnemonicButton.getItems().add(item);
|
||||
}
|
||||
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
||||
}
|
||||
|
||||
protected List<Node> createRightButtons() {
|
||||
calculateButton = new Button("Create Keystore");
|
||||
calculateButton.setDefaultButton(true);
|
||||
calculateButton.setOnAction(event -> {
|
||||
prepareImport();
|
||||
});
|
||||
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
|
||||
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided shares"));
|
||||
calculateButton.setVisible(false);
|
||||
|
||||
backButton = new Button("Back");
|
||||
backButton.setOnAction(event -> {
|
||||
lastShare();
|
||||
});
|
||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
backButton.setTooltip(new Tooltip("Display the last share added"));
|
||||
backButton.setVisible(currentShare > 0);
|
||||
|
||||
nextButton = new Button("Next");
|
||||
nextButton.setOnAction(event -> {
|
||||
nextShare();
|
||||
});
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
nextButton.setTooltip(new Tooltip("Add the next share"));
|
||||
nextButton.visibleProperty().bind(calculateButton.visibleProperty().not());
|
||||
nextButton.setDefaultButton(true);
|
||||
nextButton.setDisable(true);
|
||||
|
||||
return List.of(backButton, nextButton, calculateButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enterMnemonic(int numWords) {
|
||||
super.enterMnemonic(numWords);
|
||||
setDescription("Enter existing share");
|
||||
}
|
||||
|
||||
private void resetShares() {
|
||||
currentShare = 0;
|
||||
mnemonicShares.clear();
|
||||
}
|
||||
|
||||
private void lastShare() {
|
||||
currentShare--;
|
||||
showWordList(mnemonicShares.get(currentShare));
|
||||
}
|
||||
|
||||
private void nextShare() {
|
||||
if(currentShare == mnemonicShares.size()) {
|
||||
mnemonicShares.add(wordEntriesProperty.get());
|
||||
} else {
|
||||
mnemonicShares.set(currentShare, wordEntriesProperty.get());
|
||||
}
|
||||
|
||||
currentShare++;
|
||||
|
||||
if(currentShare < mnemonicShares.size()) {
|
||||
showWordList(mnemonicShares.get(currentShare));
|
||||
} else {
|
||||
setContent(getMnemonicWordsEntry(defaultWordSizeProperty.get(), true, true));
|
||||
}
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
boolean validSet = false;
|
||||
boolean complete = false;
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
Share.fromMnemonic(String.join(" ", wordEntriesProperty.get()));
|
||||
validChecksum = true;
|
||||
|
||||
List<List<String>> existing = new ArrayList<>(mnemonicShares);
|
||||
if(currentShare >= mnemonicShares.size()) {
|
||||
existing.add(wordEntriesProperty.get());
|
||||
}
|
||||
|
||||
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), existing, passphraseProperty.get());
|
||||
validSet = true;
|
||||
complete = true;
|
||||
} catch(MnemonicException e) {
|
||||
invalidLabel.setText(e.getTitle());
|
||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
||||
} catch(Slip39.Slip39ProgressException e) {
|
||||
validSet = true;
|
||||
invalidLabel.setText(e.getTitle());
|
||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException mnemonicException) {
|
||||
invalidLabel.setText(mnemonicException.getTitle());
|
||||
invalidLabel.setTooltip(new Tooltip(mnemonicException.getMessage()));
|
||||
} else {
|
||||
invalidLabel.setText("Import Error");
|
||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
calculateButton.setVisible(complete);
|
||||
backButton.setVisible(currentShare > 0 && !complete);
|
||||
nextButton.setDisable(!validChecksum || !validSet);
|
||||
validLabel.setVisible(complete);
|
||||
validLabel.setText(mnemonicShares.isEmpty() ? "Valid checksum" : "Completed share set");
|
||||
invalidLabel.setVisible(!complete && !empty);
|
||||
invalidLabel.setGraphic(validChecksum && validSet ? getIncompleteGlyph() : GlyphUtils.getFailureGlyph());
|
||||
}
|
||||
|
||||
private void createImportButton() {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
importButton.getStyleClass().add("default-button");
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(getDefaultDerivation(), false);
|
||||
});
|
||||
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(derivation, false);
|
||||
});
|
||||
importButton.getItems().add(item);
|
||||
}
|
||||
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
importButton.setVisible(false);
|
||||
}
|
||||
|
||||
private List<ChildNumber> getDefaultDerivation() {
|
||||
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
private void prepareImport() {
|
||||
nextShare();
|
||||
backButton.setVisible(false);
|
||||
|
||||
if(importKeystore(wallet.getScriptType().getDefaultDerivation(), true)) {
|
||||
setExpanded(true);
|
||||
enterMnemonicButton.setVisible(false);
|
||||
importButton.setVisible(true);
|
||||
importButton.setDisable(false);
|
||||
setDescription("Ready to import");
|
||||
showHideLink.setText("Show Derivation...");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getDerivationEntry(getDefaultDerivation()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
|
||||
importButton.setDisable(true);
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(derivation, mnemonicShares, passphraseProperty.get());
|
||||
if(!dryrun) {
|
||||
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
|
||||
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
|
||||
keystorePassphraseDialog.initOwner(this.getScene().getWindow());
|
||||
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
|
||||
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
|
||||
throw new ImportException("Re-entered passphrase did not match");
|
||||
}
|
||||
}
|
||||
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
return true;
|
||||
} catch (ImportException e) {
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) {
|
||||
errorMessage = "Invalid word list - checksum incorrect";
|
||||
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Import Error", errorMessage + ".");
|
||||
importButton.setDisable(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Node getDerivationEntry(List<ChildNumber> derivation) {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(derivationField, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
|
||||
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
|
||||
importDerivationButton.setDisable(true);
|
||||
importDerivationButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
|
||||
importKeystore(importDerivation, false);
|
||||
});
|
||||
|
||||
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().add(derivationField);
|
||||
contentBox.getChildren().add(importDerivationButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
public static Glyph getIncompleteGlyph() {
|
||||
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PLUS_CIRCLE);
|
||||
warningGlyph.getStyleClass().add("warn-icon");
|
||||
warningGlyph.setFontSize(12);
|
||||
return warningGlyph;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WordlistProvider getWordlistProvider() {
|
||||
return getWordListProvider(DeterministicSeed.Type.SLIP39);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -55,7 +55,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected List<Node> createRightButtons() {
|
||||
discoverButton = new Button("Discover Wallet");
|
||||
discoverButton.setDisable(true);
|
||||
discoverButton.setDefaultButton(true);
|
||||
discoverButton.setDefaultButton(AppServices.onlineProperty().get());
|
||||
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
|
||||
discoverButton.setOnAction(event -> {
|
||||
discoverWallet();
|
||||
@@ -66,6 +66,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
|
||||
importButton = new Button("Import Wallet");
|
||||
importButton.setDisable(true);
|
||||
importButton.setDefaultButton(!AppServices.onlineProperty().get());
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
importButton.visibleProperty().bind(discoverButton.visibleProperty().not());
|
||||
importButton.setOnAction(event -> {
|
||||
@@ -196,6 +197,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||
|
||||
Button importMnemonicButton = new Button("Import");
|
||||
importMnemonicButton.setDefaultButton(true);
|
||||
importMnemonicButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
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) {
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
getStyleClass().add("number-field");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -79,10 +79,6 @@ public class PayNymAvatar extends StackPane {
|
||||
this.paymentCodeProperty.set(paymentCode);
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
setPaymentCode(PaymentCode.fromString(paymentCode.toString()));
|
||||
}
|
||||
|
||||
public void clearPaymentCode() {
|
||||
this.paymentCodeProperty.set(null);
|
||||
}
|
||||
@@ -128,8 +124,11 @@ public class PayNymAvatar extends StackPane {
|
||||
log.debug("Requesting PayNym avatar from " + url);
|
||||
}
|
||||
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
|
||||
Image image = new Image(is, 150, 150, true, false);
|
||||
try(InputStream is = (proxy == null ? new URI(url).toURL().openStream() : new URI(url).toURL().openConnection(proxy).getInputStream())) {
|
||||
Image image = new Image(is, 150, 150, true, true);
|
||||
if(image.getException() != null) {
|
||||
throw image.getException();
|
||||
}
|
||||
paymentCodeCache.put(cacheId, image);
|
||||
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
|
||||
return image;
|
||||
|
||||
@@ -81,10 +81,7 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||
linkButton.setDisable(true);
|
||||
payNymController.linkPayNym(payNym);
|
||||
});
|
||||
|
||||
if(payNymController.isSelectLinkedOnly()) {
|
||||
getStyleClass().add("unlinked");
|
||||
}
|
||||
getStyleClass().add("unlinked");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,6 @@ public class PaymentCodeTextField extends CopyableTextField {
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
this.paymentCodeStr = paymentCode.toString();
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
private void setPaymentCodeString() {
|
||||
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
|
||||
setText(abbrevPcode);
|
||||
|
||||
@@ -407,6 +407,12 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
if(total - fee <= dustThreshold) {
|
||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||
return;
|
||||
} else {
|
||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds (" + total + " sats) for a transaction at this fee rate." +
|
||||
"\n\nContinue with a minimum fee rate transaction?", ButtonType.YES, ButtonType.NO);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.NO) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.sparrowwallet.hummingbird.LegacyUREncoder;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
@@ -42,7 +43,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
|
||||
private static final int MIN_FRAGMENT_LENGTH = 10;
|
||||
|
||||
private static final int ANIMATION_PERIOD_MILLIS = 200;
|
||||
private static final double ANIMATION_PERIOD_MILLIS = 200d;
|
||||
|
||||
private static final int DEFAULT_QR_SIZE = 580;
|
||||
private static final int REDUCED_QR_SIZE = 520;
|
||||
@@ -100,6 +101,16 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
qrImageView = new ImageView();
|
||||
stackPane.getChildren().add(qrImageView);
|
||||
|
||||
qrImageView.setOnScroll(scrollEvent -> {
|
||||
if(animateQRService != null && animateQRService.isRunning() && scrollEvent.getDeltaY() != 0) {
|
||||
Duration duration = animateQRService.getPeriod();
|
||||
Duration newDuration = scrollEvent.getDeltaY() > 0 ? duration.multiply(1.1) : duration.multiply(0.9);
|
||||
if(newDuration.lessThan(Duration.millis(ANIMATION_PERIOD_MILLIS*10)) && newDuration.greaterThan(Duration.millis(ANIMATION_PERIOD_MILLIS/2))) {
|
||||
animateQRService.setPeriod(newDuration);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
|
||||
nextPart();
|
||||
@@ -159,6 +170,11 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
qrImageView.setImage(getQrCode(data));
|
||||
|
||||
if(qrImageView.getImage() == null) {
|
||||
Label warning = new Label("Message is too long for display as a QR code");
|
||||
stackPane.getChildren().add(warning);
|
||||
}
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
|
||||
@@ -249,6 +265,10 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUseBbqrEncoding() {
|
||||
return useBbqrEncoding;
|
||||
}
|
||||
|
||||
private void setUseBbqrEncoding(boolean useBbqrEncoding) {
|
||||
if(useBbqrEncoding) {
|
||||
this.useBbqrEncoding = true;
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.github.sarxos.webcam.*;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||
import com.sparrowwallet.drongo.address.P2SHAddress;
|
||||
@@ -28,6 +25,8 @@ import com.sparrowwallet.hummingbird.URDecoder;
|
||||
import com.sparrowwallet.hummingbird.registry.pathcomponent.IndexPathComponent;
|
||||
import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WebcamResolutionChangedEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder;
|
||||
@@ -95,7 +94,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
|
||||
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
|
||||
webcamService.setRestartOnFailure(false);
|
||||
WebcamView webcamView = new WebcamView(webcamService);
|
||||
WebcamView webcamView = new WebcamView(webcamService, Config.get().isMirrorCapture());
|
||||
|
||||
final DialogPane dialogPane = new QRScanDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
@@ -138,7 +137,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
while(nested.getCause() != null) {
|
||||
nested = nested.getCause();
|
||||
}
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS &&
|
||||
if(OsType.getCurrent() == OsType.WINDOWS &&
|
||||
nested.getMessage().startsWith("Library 'OpenIMAJGrabber' was not loaded successfully from file")) {
|
||||
exception = new WebcamDependencyException("Your system is missing a dependency required for the webcam. Follow the link below for more details.\n\n[https://sparrowwallet.com/docs/faq.html#your-system-is-missing-a-dependency-for-the-webcam]", exception);
|
||||
} else if(nested.getMessage().startsWith("Cannot start native grabber") && Config.get().getWebcamDevice() != null) {
|
||||
@@ -153,6 +152,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
|
||||
if(newResolution != null) {
|
||||
setHeight(newResolution == WebcamResolution.HD ? (getHeight() - 100) : (getHeight() + 100));
|
||||
EventManager.get().post(new WebcamResolutionChangedEvent(newResolution == WebcamResolution.HD));
|
||||
}
|
||||
webcamService.cancel();
|
||||
});
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.csvreader.CsvWriter;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.wallet.TableType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -20,10 +30,11 @@ import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
public class SearchWalletDialog extends Dialog<Entry> {
|
||||
private static final Logger log = LoggerFactory.getLogger(SearchWalletDialog.class);
|
||||
@@ -48,7 +59,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("search.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText(showWallet ? "Search All Wallets" : "Search Wallet");
|
||||
dialogPane.setHeaderText(showWallet ? "Search All Wallets" : "Search Wallet " + walletForms.get(0).getMasterWallet().getName());
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
if(!image.isError()) {
|
||||
@@ -76,11 +87,12 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
form.getChildren().add(fieldset);
|
||||
|
||||
results = new CoinTreeTable();
|
||||
results.setTableType(TableType.SEARCH_WALLET);
|
||||
results.setShowRoot(false);
|
||||
results.setPrefWidth(showWallet || showAccount ? 950 : 850);
|
||||
results.setUnitFormat(walletForms.iterator().next().getWallet());
|
||||
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
results.setPlaceholder(new Label("No results"));
|
||||
results.setEditable(true);
|
||||
|
||||
if(showWallet) {
|
||||
TreeTableColumn<Entry, String> walletColumn = new TreeTableColumn<>("Wallet");
|
||||
@@ -117,7 +129,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
return param.getValue().getValue().labelProperty();
|
||||
});
|
||||
labelCol.setCellFactory(p -> new SearchLabelCell());
|
||||
labelCol.setCellFactory(p -> new LabelCell());
|
||||
results.getColumns().add(labelCol);
|
||||
|
||||
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
|
||||
@@ -130,12 +142,20 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
vBox.getChildren().addAll(form, results);
|
||||
dialogPane.setContent(vBox);
|
||||
|
||||
ButtonType exportButtonType = new ButtonType("Export CSV", ButtonBar.ButtonData.LEFT);
|
||||
ButtonType showButtonType = new javafx.scene.control.ButtonType("Show", ButtonBar.ButtonData.APPLY);
|
||||
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType, showButtonType);
|
||||
dialogPane.getButtonTypes().addAll(exportButtonType, cancelButtonType, showButtonType);
|
||||
|
||||
Button showButton = (Button) dialogPane.lookupButton(showButtonType);
|
||||
Button exportButton = (Button)dialogPane.lookupButton(exportButtonType);
|
||||
exportButton.setGraphic(GlyphUtils.getDownArrowGlyph());
|
||||
exportButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
event.consume();
|
||||
exportResults(showWallet);
|
||||
});
|
||||
|
||||
Button showButton = (Button)dialogPane.lookupButton(showButtonType);
|
||||
showButton.setDefaultButton(true);
|
||||
showButton.setDisable(true);
|
||||
|
||||
@@ -147,52 +167,56 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
});
|
||||
|
||||
search.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
searchWallets(newValue.toLowerCase(Locale.ROOT));
|
||||
searchWallets(newValue);
|
||||
});
|
||||
|
||||
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.getFirst().getWallet(), Collections.emptyList());
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
results.setRoot(rootItem);
|
||||
|
||||
setResizable(true);
|
||||
results.setupColumnWidths();
|
||||
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Platform.runLater(search::requestFocus);
|
||||
}
|
||||
|
||||
private void searchWallets(String searchText) {
|
||||
List<Entry> matchingEntries = new ArrayList<>();
|
||||
public List<WalletForm> getWalletForms() {
|
||||
return walletForms;
|
||||
}
|
||||
|
||||
if(!searchText.isEmpty()) {
|
||||
Long searchValue = getSearchValue(searchText);
|
||||
Address searchAddress = getSearchAddress(searchText);
|
||||
private void searchWallets(String searchPhrase) {
|
||||
Set<Entry> matchingEntries = new LinkedHashSet<>();
|
||||
|
||||
for(WalletForm walletForm : walletForms) {
|
||||
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue) ||
|
||||
(searchAddress != null && transactionEntry.getBlockTransaction().getTransaction().getOutputs().stream().map(output -> output.getScript().getToAddress()).filter(Objects::nonNull).anyMatch(address -> address.equals(searchAddress)))) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!searchPhrase.isEmpty()) {
|
||||
Set<String> searchWords = new LinkedHashSet<>(Arrays.stream(searchPhrase.split("\\s+"))
|
||||
.filter(text -> isAddress(text) || isHash(text) || isHashIndex(text)).toList());
|
||||
String freeText = removeOccurrences(searchPhrase, searchWords).trim();
|
||||
if(!freeText.isEmpty()) {
|
||||
searchWords.add(freeText);
|
||||
}
|
||||
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().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)) {
|
||||
for(String searchText : searchWords) {
|
||||
Long searchValue = getSearchValue(searchText);
|
||||
Address searchAddress = getSearchAddress(searchText);
|
||||
searchText = searchText.toLowerCase(Locale.ROOT);
|
||||
|
||||
for(WalletForm walletForm : walletForms) {
|
||||
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue) ||
|
||||
(searchAddress != null && transactionEntry.getBlockTransaction().getTransaction().getOutputs().stream().map(output -> output.getScript().getToAddress()).filter(Objects::nonNull).anyMatch(address -> address.equals(searchAddress)))) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(WalletForm nestedWalletForm : walletForm.getNestedWalletForms()) {
|
||||
for(KeyPurpose keyPurpose : nestedWalletForm.getWallet().getWalletKeyPurposes()) {
|
||||
NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose);
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||
@@ -203,22 +227,38 @@ 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().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);
|
||||
for(WalletForm nestedWalletForm : walletForm.getNestedWalletForms()) {
|
||||
for(KeyPurpose keyPurpose : nestedWalletForm.getWallet().getWalletKeyPurposes()) {
|
||||
NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
if(hashIndexEntry.getBlockTransaction().getHash().toString().toLowerCase(Locale.ROOT).equals(searchText) ||
|
||||
hashIndexEntry.getHashIndex().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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.iterator().next().getWallet(), matchingEntries);
|
||||
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.iterator().next().getWallet(), new ArrayList<>(matchingEntries));
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
results.setRoot(rootItem);
|
||||
}
|
||||
@@ -239,6 +279,95 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAddress(String text) {
|
||||
try {
|
||||
Address.fromString(text);
|
||||
return true;
|
||||
} catch(InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHash(String text) {
|
||||
return text.length() == 64 && Utils.isHex(text);
|
||||
}
|
||||
|
||||
private boolean isHashIndex(String text) {
|
||||
String[] parts = text.split(":");
|
||||
if(parts.length == 2 && isHash(parts[0])) {
|
||||
try {
|
||||
Integer.parseInt(parts[1]);
|
||||
return true;
|
||||
} catch(NumberFormatException e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private String removeOccurrences(String inputString, Collection<String> stringsToRemove) {
|
||||
for(String str : stringsToRemove) {
|
||||
inputString = inputString.replaceAll("(?i)" + str, "");
|
||||
}
|
||||
|
||||
return inputString;
|
||||
}
|
||||
|
||||
public void exportResults(boolean showWallet) {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Export search results to CSV");
|
||||
fileChooser.setInitialFileName(getDialogPane().getHeaderText() + ".csv");
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8);
|
||||
List<String> headers = new ArrayList<>(List.of("Wallet", "Account", "Type", "Date", "Txid / Address / Output", "Label", "Value"));
|
||||
if(!showWallet) {
|
||||
headers.remove(0);
|
||||
}
|
||||
writer.writeRecord(headers.toArray(new String[0]));
|
||||
for(TreeItem<Entry> item : results.getRoot().getChildren()) {
|
||||
Entry entry = item.getValue();
|
||||
if(showWallet) {
|
||||
writer.write(entry.getWallet().getMasterName());
|
||||
}
|
||||
writer.write(entry.getWallet().getDisplayName());
|
||||
writer.write(entry.getEntryType());
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
writer.write(transactionEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate()));
|
||||
writer.write(transactionEntry.getBlockTransaction().getHash().toString());
|
||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
||||
writer.write("");
|
||||
writer.write(nodeEntry.getAddress().toString());
|
||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
writer.write(hashIndexEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(hashIndexEntry.getBlockTransaction().getDate()));
|
||||
writer.write(hashIndexEntry.getHashIndex().toString());
|
||||
} else {
|
||||
writer.write("");
|
||||
writer.write("");
|
||||
}
|
||||
writer.write(entry.getLabel());
|
||||
writer.write(getCoinValue(entry.getValue() == null ? 0 : entry.getValue()));
|
||||
writer.endRecord();
|
||||
}
|
||||
writer.close();
|
||||
} catch(IOException e) {
|
||||
log.error("Error exporting search results as CSV", e);
|
||||
AppServices.showErrorDialog("Error exporting search results as CSV", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getCoinValue(Long value) {
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
return BitcoinUnit.BTC.equals(results.getBitcoinUnit()) ? format.tableFormatBtcValue(value) : String.format(Locale.ENGLISH, "%d", value);
|
||||
}
|
||||
|
||||
private static class SearchWalletEntry extends Entry {
|
||||
public SearchWalletEntry(Wallet wallet, List<Entry> entries) {
|
||||
super(wallet, wallet.getName(), entries);
|
||||
@@ -264,15 +393,19 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
@Override
|
||||
protected void updateItem(Entry entry, boolean empty) {
|
||||
super.updateItem(entry, empty);
|
||||
setContextMenu(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SearchLabelCell extends LabelCell {
|
||||
@Override
|
||||
public void updateItem(String label, boolean empty) {
|
||||
super.updateItem(label, empty);
|
||||
setContextMenu(null);
|
||||
ContextMenu copyMenu;
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
copyMenu = new TransactionContextMenu(getText(), transactionEntry.getBlockTransaction());
|
||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
||||
copyMenu = new AddressContextMenu(nodeEntry.getAddress(), nodeEntry.getOutputDescriptor(), null, false, null);
|
||||
} else if(entry instanceof UtxoEntry utxoEntry) {
|
||||
copyMenu = new HashIndexEntryContextMenu(null, utxoEntry);
|
||||
} else {
|
||||
copyMenu = new ContextMenu();
|
||||
}
|
||||
copyMenu.getItems().removeIf(menuItem -> !menuItem.getText().startsWith("Copy"));
|
||||
setContextMenu(copyMenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.SeedQR;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@@ -17,7 +18,7 @@ public class SeedDisplayDialog extends Dialog<Void> {
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
int lines = decryptedKeystore.getSeed().getMnemonicCode().size() / 3;
|
||||
int lines = Math.ceilDiv(decryptedKeystore.getSeed().getMnemonicCode().size(), 3);
|
||||
int height = lines * 40;
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
@@ -43,15 +44,19 @@ public class SeedDisplayDialog extends Dialog<Void> {
|
||||
|
||||
stackPane.getChildren().addAll(anchorPane);
|
||||
|
||||
final ButtonType seedQRButtonType = new javafx.scene.control.ButtonType("Show SeedQR", ButtonBar.ButtonData.LEFT);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(seedQRButtonType, cancelButtonType);
|
||||
if(decryptedKeystore.getSeed().getType() == DeterministicSeed.Type.BIP39) {
|
||||
final ButtonType seedQRButtonType = new javafx.scene.control.ButtonType("Show SeedQR", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(seedQRButtonType);
|
||||
|
||||
Button seedQRButton = (Button)dialogPane.lookupButton(seedQRButtonType);
|
||||
seedQRButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
event.consume();
|
||||
showSeedQR(decryptedKeystore);
|
||||
});
|
||||
Button seedQRButton = (Button)dialogPane.lookupButton(seedQRButtonType);
|
||||
seedQRButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
event.consume();
|
||||
showSeedQR(decryptedKeystore);
|
||||
});
|
||||
}
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().add(cancelButtonType);
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(150 + height);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import org.fxmisc.richtext.CodeArea;
|
||||
|
||||
public class SelectableCodeArea extends CodeArea {
|
||||
public SelectableCodeArea() {
|
||||
super();
|
||||
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
MenuItem copy = new MenuItem("Copy");
|
||||
copy.setDisable(true);
|
||||
copy.setOnAction(event -> {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(getSelectedText());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
MenuItem copyAll = new MenuItem("Copy All");
|
||||
copyAll.setOnAction(event -> {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(getText());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
contextMenu.getItems().addAll(copy, copyAll);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
selectedTextProperty().addListener((observable, oldValue, newValue) -> {
|
||||
copy.setDisable(newValue.isEmpty());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.csvreader.CsvReader;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
@@ -20,7 +21,6 @@ import javafx.stage.FileChooser;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.control.spreadsheet.*;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -105,7 +105,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
SpreadsheetCell amountCell = SpreadsheetCellType.DOUBLE.createCell(row, 1, 1, 1, amount < 0 ? null : amount);
|
||||
amountCell.setFormat(bitcoinUnit == BitcoinUnit.BTC ? "0.00000000" : "###,###");
|
||||
amountCell.getStyleClass().add("number-value");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
amountCell.getStyleClass().add("number-field");
|
||||
}
|
||||
list.add(amountCell);
|
||||
@@ -161,7 +161,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open CSV");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("CSV", "*.csv")
|
||||
);
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.OptionalDouble;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TitledDescriptionPane extends TitledPane {
|
||||
private Label mainLabel;
|
||||
@@ -127,25 +127,45 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
}
|
||||
|
||||
protected Node getContentBox(String message) {
|
||||
Label details = new Label(message);
|
||||
details.setWrapText(true);
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
// Create the VBox to hold text and Hyperlink components
|
||||
VBox contentBox = new VBox();
|
||||
contentBox.setAlignment(Pos.TOP_LEFT);
|
||||
contentBox.getChildren().add(details);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefWidth(400); // Set preferred width for wrapping
|
||||
contentBox.setMinHeight(60);
|
||||
|
||||
double width = TextUtils.computeTextWidth(details.getFont(), message, 0.0D);
|
||||
double numLines = Math.max(1, Math.ceil(width / 400d));
|
||||
// Define the regex pattern to match URLs
|
||||
String urlPattern = "(\\[https?://\\S+])";
|
||||
Pattern pattern = Pattern.compile(urlPattern);
|
||||
Matcher matcher = pattern.matcher(message);
|
||||
|
||||
//Handle long words like txids
|
||||
OptionalDouble maxWordLength = Arrays.stream(message.split(" ")).mapToDouble(word -> TextUtils.computeTextWidth(details.getFont(), message, 0.0D)).max();
|
||||
if(maxWordLength.isPresent() && maxWordLength.getAsDouble() > 300.0) {
|
||||
numLines += 1.0;
|
||||
// StringBuilder to track the non-URL text
|
||||
int lastMatchEnd = 0;
|
||||
|
||||
// Iterate through the matches and build the components
|
||||
while (matcher.find()) {
|
||||
// Add the text before the URL as a normal Label
|
||||
if (matcher.start() > lastMatchEnd) {
|
||||
String nonUrlText = message.substring(lastMatchEnd, matcher.start());
|
||||
Label textLabel = createWrappedLabel(nonUrlText);
|
||||
contentBox.getChildren().add(textLabel);
|
||||
}
|
||||
|
||||
// Extract the URL and create a Hyperlink for it
|
||||
String url = matcher.group(1).replaceAll("\\[", "").replaceAll("\\]", "");
|
||||
Hyperlink hyperlink = createHyperlink(url);
|
||||
contentBox.getChildren().add(hyperlink);
|
||||
|
||||
// Update last match end
|
||||
lastMatchEnd = matcher.end();
|
||||
}
|
||||
|
||||
double height = Math.max(60, numLines * 20);
|
||||
contentBox.setPrefHeight(height);
|
||||
// Add remaining text after the last URL (if any)
|
||||
if (lastMatchEnd < message.length()) {
|
||||
String remainingText = message.substring(lastMatchEnd);
|
||||
Label remainingLabel = createWrappedLabel(remainingText);
|
||||
contentBox.getChildren().add(remainingLabel);
|
||||
}
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
@@ -178,4 +198,21 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
// Helper method to create a wrapped Label with a specified maxWidth
|
||||
private Label createWrappedLabel(String text) {
|
||||
Label label = new Label(text);
|
||||
label.setWrapText(true);
|
||||
label.setMaxWidth(400);
|
||||
return label;
|
||||
}
|
||||
|
||||
// Helper method to create a Hyperlink
|
||||
private Hyperlink createHyperlink(String url) {
|
||||
Hyperlink hyperlink = new Hyperlink(url);
|
||||
hyperlink.setMaxWidth(400); // Set maximum width for wrapping
|
||||
hyperlink.setWrapText(true); // Ensure text wrapping in the hyperlink
|
||||
hyperlink.setOnAction(_ -> AppServices.get().getApplication().getHostServices().showDocument(url));
|
||||
return hyperlink;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
@@ -14,7 +15,6 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -28,7 +28,7 @@ public class TorStatusLabel extends Label {
|
||||
|
||||
public TorStatusLabel() {
|
||||
getStyleClass().add("tor-status");
|
||||
setPadding(Platform.getCurrent() == Platform.WINDOWS ? new Insets(0, 0, 1, 3) : new Insets(1, 0, 0, 3));
|
||||
setPadding(OsType.getCurrent() == OsType.WINDOWS ? new Insets(0, 0, 1, 3) : new Insets(1, 0, 0, 3));
|
||||
setGraphic(getIcon());
|
||||
update();
|
||||
}
|
||||
@@ -62,7 +62,7 @@ public class TorStatusLabel extends Label {
|
||||
|
||||
private Node getIcon() {
|
||||
Glyph adjust = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ADJUST);
|
||||
adjust.setFontSize(Platform.getCurrent() == Platform.WINDOWS ? 14 : 15);
|
||||
adjust.setFontSize(OsType.getCurrent() == OsType.WINDOWS ? 14 : 15);
|
||||
adjust.setRotate(180);
|
||||
|
||||
Glyph bullseye = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BULLSEYE);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
@@ -12,11 +13,9 @@ import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||
import com.sparrowwallet.sparrow.event.SorobanInitiatedEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -46,7 +45,6 @@ import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.*;
|
||||
@@ -91,7 +89,7 @@ public class TransactionDiagram extends GridPane {
|
||||
stage.setResizable(false);
|
||||
|
||||
StackPane scenePane = new StackPane();
|
||||
if(Platform.getCurrent() == Platform.WINDOWS || Platform.getCurrent() == Platform.UNIX) {
|
||||
if(OsType.getCurrent() == OsType.WINDOWS || OsType.getCurrent() == OsType.UNIX) {
|
||||
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||
}
|
||||
|
||||
@@ -246,19 +244,9 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
|
||||
boolean addUserSet = getOptimizationStrategy() == OptimizationStrategy.PRIVACY && SorobanServices.canWalletMix(walletTx.getWallet())
|
||||
&& walletTx.getPayments().size() == 1
|
||||
&& (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>();
|
||||
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) {
|
||||
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, addUserSet ? 2 : walletTx.getSelectedUtxoSets().size()));
|
||||
}
|
||||
|
||||
if(addUserSet && displayedUtxoSets.size() == 1) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> addUserUtxoSet = new HashMap<>();
|
||||
addUserUtxoSet.put(new AddUserBlockTransactionHashIndex(!walletTx.isTwoPersonCoinjoin()), null);
|
||||
displayedUtxoSets.add(addUserUtxoSet);
|
||||
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, walletTx.getSelectedUtxoSets().size()));
|
||||
}
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> paddedUtxoSets = new ArrayList<>();
|
||||
@@ -339,11 +327,9 @@ public class TransactionDiagram extends GridPane {
|
||||
double setHeight = (height / numSets) - 5;
|
||||
for(int set = 0; set < numSets; set++) {
|
||||
boolean externalUserSet = displayedUtxoSets.get(set).values().stream().anyMatch(Objects::nonNull);
|
||||
boolean addUserSet = displayedUtxoSets.get(set).keySet().stream().anyMatch(ref -> ref instanceof AddUserBlockTransactionHashIndex);
|
||||
if(externalUserSet || addUserSet) {
|
||||
boolean replace = !isFinal() && set > 0 && SorobanServices.canWalletMix(walletTx.getWallet());
|
||||
Glyph bracketGlyph = !replace && walletTx.isCoinControlUsed() ? getLockGlyph() : (addUserSet ? getUserAddGlyph() : getCoinsGlyph(replace));
|
||||
String tooltipText = addUserSet ? "Click to add a mix partner" : (walletTx.getWallet().getFullDisplayName() + (replace ? "\nClick to replace with a mix partner" : ""));
|
||||
if(externalUserSet) {
|
||||
Glyph bracketGlyph = walletTx.isCoinControlUsed() ? getLockGlyph() : getCoinsGlyph();
|
||||
String tooltipText = walletTx.getWallet().getFullDisplayName();
|
||||
StackPane stackPane = getBracket(width, setHeight, bracketGlyph, tooltipText);
|
||||
allBrackets.getChildren().add(stackPane);
|
||||
} else {
|
||||
@@ -446,8 +432,14 @@ public class TransactionDiagram extends GridPane {
|
||||
if(walletNode != null) {
|
||||
inputValue = input.getValue();
|
||||
Wallet nodeWallet = walletNode.getWallet();
|
||||
tooltip.setText("Spending " + getSatsValue(inputValue) + " sats from " + (isFinal() ? nodeWallet.getFullDisplayName() : (nodeWallet.isNested() ? nodeWallet.getDisplayName() : "")) + " " + walletNode + "\n" +
|
||||
input.getHashAsString() + ":" + input.getIndex() + "\n" + walletNode.getAddress());
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
joiner.add("Spending " + getSatsValue(inputValue) + " sats from " + (isFinal() ? nodeWallet.getFullDisplayName() : (nodeWallet.isNested() ? nodeWallet.getDisplayName() : "")) + " " + walletNode);
|
||||
joiner.add(input.getHashAsString() + ":" + input.getIndex());
|
||||
joiner.add(walletNode.getAddress().toString());
|
||||
if(input.getLabel() != null) {
|
||||
joiner.add(input.getLabel());
|
||||
}
|
||||
tooltip.setText(joiner.toString());
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
|
||||
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
||||
@@ -474,14 +466,6 @@ public class TransactionDiagram extends GridPane {
|
||||
tooltip.setText(joiner.toString());
|
||||
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
|
||||
tooltip.setText("");
|
||||
} else if(input instanceof AddUserBlockTransactionHashIndex) {
|
||||
tooltip.setText("");
|
||||
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getFeeWarningGlyph());
|
||||
label.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
} else {
|
||||
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
|
||||
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
|
||||
@@ -570,7 +554,7 @@ public class TransactionDiagram extends GridPane {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("input-line");
|
||||
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex || inputs.get(numUtxos-i) instanceof AddUserBlockTransactionHashIndex) {
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex) {
|
||||
curve.getStyleClass().add("input-dashed-line");
|
||||
} else if(inputs.get(numUtxos-i) instanceof InvisibleBlockTransactionHashIndex) {
|
||||
continue;
|
||||
@@ -952,46 +936,10 @@ public class TransactionDiagram extends GridPane {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Glyph getUserAddGlyph() {
|
||||
Glyph userAddGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER_PLUS);
|
||||
userAddGlyph.getStyleClass().add("useradd-icon");
|
||||
userAddGlyph.setFontSize(12);
|
||||
userAddGlyph.setOnMouseEntered(event -> {
|
||||
userAddGlyph.setFontSize(18);
|
||||
});
|
||||
userAddGlyph.setOnMouseExited(event -> {
|
||||
userAddGlyph.setFontSize(12);
|
||||
});
|
||||
userAddGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
return userAddGlyph;
|
||||
}
|
||||
|
||||
private Glyph getCoinsGlyph(boolean allowReplacement) {
|
||||
private Glyph getCoinsGlyph() {
|
||||
Glyph coinsGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
|
||||
coinsGlyph.setFontSize(12);
|
||||
if(allowReplacement) {
|
||||
coinsGlyph.getStyleClass().add("coins-replace-icon");
|
||||
coinsGlyph.setOnMouseEntered(event -> {
|
||||
coinsGlyph.setIcon(FontAwesome5.Glyph.USER_PLUS);
|
||||
coinsGlyph.setFontSize(18);
|
||||
});
|
||||
coinsGlyph.setOnMouseExited(event -> {
|
||||
coinsGlyph.setIcon(FontAwesome5.Glyph.COINS);
|
||||
coinsGlyph.setFontSize(12);
|
||||
});
|
||||
coinsGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
} else {
|
||||
coinsGlyph.getStyleClass().add("coins-icon");
|
||||
}
|
||||
|
||||
coinsGlyph.getStyleClass().add("coins-icon");
|
||||
return coinsGlyph;
|
||||
}
|
||||
|
||||
@@ -1094,20 +1042,6 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddUserBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
private final boolean required;
|
||||
|
||||
public AddUserBlockTransactionHashIndex(boolean required) {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return "Add Mix Partner" + (required ? "" : "?");
|
||||
}
|
||||
}
|
||||
|
||||
public static class AdditionalPayment extends Payment {
|
||||
private final List<Payment> additionalPayments;
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.TableType;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
|
||||
public class TransactionsTreeTable extends CoinTreeTable {
|
||||
public void initialize(WalletTransactionsEntry rootEntry) {
|
||||
@@ -49,8 +49,8 @@ public class TransactionsTreeTable extends CoinTreeTable {
|
||||
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
setSortColumn(0, TreeTableColumn.SortType.DESCENDING);
|
||||
setupColumnWidths();
|
||||
setupColumnSort(0, TreeTableColumn.SortType.DESCENDING);
|
||||
}
|
||||
|
||||
public void updateAll(WalletTransactionsEntry rootEntry) {
|
||||
@@ -60,13 +60,13 @@ public class TransactionsTreeTable extends CoinTreeTable {
|
||||
setRoot(rootItem);
|
||||
rootItem.setExpanded(true);
|
||||
|
||||
setSortColumn(0, TreeTableColumn.SortType.DESCENDING);
|
||||
resetSortColumn();
|
||||
}
|
||||
|
||||
public void updateHistory() {
|
||||
//Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required
|
||||
sort();
|
||||
setSortColumn(0, TreeTableColumn.SortType.DESCENDING);
|
||||
resetSortColumn();
|
||||
}
|
||||
|
||||
public void updateLabel(Entry entry) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
@@ -31,7 +32,7 @@ public class TrayManager {
|
||||
|
||||
try {
|
||||
List<Image> imgList = new ArrayList<>();
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
if(OsType.getCurrent() == OsType.WINDOWS) {
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small.png")));
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@2x.png")));
|
||||
imgList.add(ImageIO.read(getClass().getResource("/image/sparrow-black-small@3x.png")));
|
||||
|
||||
@@ -27,8 +27,8 @@ public class UsbStatusButton extends MenuButton {
|
||||
public void setDevices(List<Device> devices) {
|
||||
for(Device device : devices) {
|
||||
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
|
||||
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T ||
|
||||
device.getModel() == WalletModel.TREZOR_SAFE_3 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
|
||||
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.TREZOR_SAFE_3 ||
|
||||
device.getModel() == WalletModel.TREZOR_SAFE_5 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
|
||||
deviceItem = new Menu(device.getModel().toDisplayString());
|
||||
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
|
||||
toggleItem.setOnAction(event -> {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.TableType;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
@@ -82,8 +82,8 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
|
||||
setupColumnWidths();
|
||||
setupColumnSort(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
|
||||
|
||||
getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
}
|
||||
@@ -95,14 +95,14 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
setRoot(rootItem);
|
||||
rootItem.setExpanded(true);
|
||||
|
||||
setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
|
||||
resetSortColumn();
|
||||
}
|
||||
|
||||
public void updateHistory() {
|
||||
//Utxo entries should have already been updated, so only a resort required
|
||||
if(!getRoot().getChildren().isEmpty()) {
|
||||
sort();
|
||||
setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
|
||||
resetSortColumn();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,10 @@ import javafx.scene.shape.Circle;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
@@ -137,7 +138,7 @@ public class WalletIcon extends StackPane {
|
||||
}
|
||||
|
||||
private void addWalletIcon(String walletId) {
|
||||
Image image = new Image(PROTOCOL + ":" + walletId + "?" + QUERY, WIDTH, HEIGHT, true, false);
|
||||
Image image = new Image(PROTOCOL + ":" + walletId.replaceAll(" ", "%20").replaceAll("#", "%23") + "?" + QUERY, WIDTH, HEIGHT, true, false);
|
||||
getChildren().clear();
|
||||
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
|
||||
circle.setFill(new ImagePattern(image));
|
||||
@@ -176,6 +177,8 @@ public class WalletIcon extends StackPane {
|
||||
}
|
||||
|
||||
public static class WalletIconStreamHandler extends URLStreamHandler {
|
||||
private static final Logger log = LoggerFactory.getLogger(WalletIconStreamHandler.class);
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) throws IOException {
|
||||
return new URLConnection(url) {
|
||||
@@ -186,17 +189,19 @@ public class WalletIcon extends StackPane {
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
String walletId = url.getPath();
|
||||
String walletId = url.getPath().replaceAll("%20", " ").replaceAll("%23", "#");
|
||||
String query = url.getQuery();
|
||||
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
if(wallet == null) {
|
||||
throw new IOException("Cannot find wallet for wallet id " + walletId);
|
||||
log.warn("Cannot find wallet for wallet id " + walletId);
|
||||
return getFallbackIconStream();
|
||||
}
|
||||
|
||||
if(query.startsWith(QUERY)) {
|
||||
if(wallet.getWalletConfig() == null || wallet.getWalletConfig().getIconData() == null) {
|
||||
throw new IOException("No icon data for " + walletId);
|
||||
log.warn("No icon data for " + walletId);
|
||||
return getFallbackIconStream();
|
||||
}
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(wallet.getWalletConfig().getIconData());
|
||||
@@ -207,7 +212,12 @@ public class WalletIcon extends StackPane {
|
||||
}
|
||||
}
|
||||
|
||||
throw new MalformedURLException("Cannot load url " + url);
|
||||
log.warn("Cannot load url " + url);
|
||||
return getFallbackIconStream();
|
||||
}
|
||||
|
||||
private static InputStream getFallbackIconStream() {
|
||||
return WalletIconStreamHandler.class.getResourceAsStream("/image/sparrow.png");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
importAccordion = new Accordion();
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY(), new Krux(), new AirGapVault());
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(),
|
||||
new GordianSeedTool(), new SeedSigner(), new SpecterDIY(), new Krux(), new AirGapVault(), new Samourai());
|
||||
for(KeystoreFileImport importer : keystoreImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
@@ -59,7 +60,8 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
}
|
||||
}
|
||||
|
||||
List<WalletImport> walletImporters = new ArrayList<>(List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new JadeMultisig()));
|
||||
List<WalletImport> walletImporters = new ArrayList<>(List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(),
|
||||
new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new JadeMultisig(), new PassportMultisig(), new SpecterDIYMultisig()));
|
||||
if(!selectedWalletForms.isEmpty()) {
|
||||
walletImporters.add(new WalletLabels(selectedWalletForms));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
@@ -56,6 +58,11 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(460);
|
||||
dialogPane.setPrefHeight(requestBirthDate ? 250 : 200);
|
||||
dialogPane.getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (event.getCode() == KeyCode.ESCAPE) {
|
||||
WalletNameDialog.this.close();
|
||||
}
|
||||
});
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.TableType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||
@@ -18,17 +19,21 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class WalletSummaryDialog extends Dialog<Void> {
|
||||
public WalletSummaryDialog(List<WalletForm> walletForms) {
|
||||
if(walletForms.isEmpty()) {
|
||||
public WalletSummaryDialog(List<List<WalletForm>> summaryWalletFormsList) {
|
||||
List<List<WalletForm>> walletFormsList = new ArrayList<>(summaryWalletFormsList);
|
||||
walletFormsList.removeIf(List::isEmpty);
|
||||
if(walletFormsList.isEmpty()) {
|
||||
throw new IllegalArgumentException("No wallets selected to summarize");
|
||||
}
|
||||
|
||||
Wallet masterWallet = walletForms.get(0).getMasterWallet();
|
||||
boolean allOpenWallets = walletFormsList.size() > 1;
|
||||
List<Wallet> masterWallets = walletFormsList.stream().map(walletForms -> walletForms.get(0).getMasterWallet()).toList();
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
@@ -37,7 +42,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/transactions.css").toExternalForm());
|
||||
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText("Wallet Summary for " + masterWallet.getName());
|
||||
dialogPane.setHeaderText("Wallet Summary for " + (allOpenWallets ? "All Open Wallets" : masterWallets.get(0).getName()));
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
if(!image.isError()) {
|
||||
@@ -50,6 +55,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
HBox hBox = new HBox(40);
|
||||
|
||||
CoinTreeTable table = new CoinTreeTable();
|
||||
table.setTableType(TableType.WALLET_SUMMARY);
|
||||
|
||||
TreeTableColumn<Entry, String> nameColumn = new TreeTableColumn<>("Wallet");
|
||||
nameColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
@@ -64,7 +70,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
});
|
||||
balanceColumn.setCellFactory(p -> new CoinCell());
|
||||
table.getColumns().add(balanceColumn);
|
||||
table.setUnitFormat(masterWallet);
|
||||
table.setUnitFormat(masterWallets.get(0));
|
||||
|
||||
CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate();
|
||||
if(currencyRate != null && currencyRate.isAvailable() && Config.get().getExchangeSource() != ExchangeSource.NONE) {
|
||||
@@ -77,19 +83,27 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
table.setCurrencyRate(currencyRate);
|
||||
}
|
||||
|
||||
SummaryEntry rootEntry = new SummaryEntry(walletForms);
|
||||
Entry rootEntry = allOpenWallets ? new AllSummaryEntry(walletFormsList) : new SummaryEntry(walletFormsList.get(0));
|
||||
TreeItem<Entry> rootItem = new TreeItem<>(rootEntry);
|
||||
for(Entry childEntry : rootEntry.getChildren()) {
|
||||
TreeItem<Entry> childItem = new TreeItem<>(childEntry);
|
||||
rootItem.getChildren().add(childItem);
|
||||
childItem.getChildren().add(new TreeItem<>(new UnconfirmedEntry((WalletTransactionsEntry)childEntry)));
|
||||
if(allOpenWallets) {
|
||||
for(Entry walletEntry : childEntry.getChildren()) {
|
||||
TreeItem<Entry> walletItem = new TreeItem<>(walletEntry);
|
||||
childItem.getChildren().add(walletItem);
|
||||
walletItem.getChildren().add(new TreeItem<>(new UnconfirmedEntry((WalletTransactionsEntry)walletEntry)));
|
||||
}
|
||||
} else {
|
||||
childItem.getChildren().add(new TreeItem<>(new UnconfirmedEntry((WalletTransactionsEntry)childEntry)));
|
||||
}
|
||||
}
|
||||
|
||||
table.setShowRoot(true);
|
||||
table.setRoot(rootItem);
|
||||
rootItem.setExpanded(true);
|
||||
|
||||
table.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
table.setupColumnWidths();
|
||||
table.setPrefWidth(450);
|
||||
|
||||
VBox vBox = new VBox();
|
||||
@@ -97,6 +111,14 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
|
||||
hBox.getChildren().add(vBox);
|
||||
|
||||
Wallet balanceWallet;
|
||||
if(allOpenWallets) {
|
||||
balanceWallet = new Wallet();
|
||||
balanceWallet.getChildWallets().addAll(masterWallets.stream().flatMap(mws -> mws.getAllWallets().stream()).toList());
|
||||
} else {
|
||||
balanceWallet = masterWallets.get(0);
|
||||
}
|
||||
|
||||
NumberAxis xAxis = new NumberAxis();
|
||||
xAxis.setSide(Side.BOTTOM);
|
||||
xAxis.setForceZeroInRange(false);
|
||||
@@ -104,7 +126,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
NumberAxis yAxis = new NumberAxis();
|
||||
yAxis.setSide(Side.LEFT);
|
||||
BalanceChart balanceChart = new BalanceChart(xAxis, yAxis);
|
||||
balanceChart.initialize(new WalletTransactionsEntry(masterWallet, true));
|
||||
balanceChart.initialize(new WalletTransactionsEntry(balanceWallet, true));
|
||||
balanceChart.setAnimated(false);
|
||||
balanceChart.setLegendVisible(false);
|
||||
balanceChart.setVerticalGridLinesVisible(false);
|
||||
@@ -120,6 +142,32 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
public static class AllSummaryEntry extends Entry {
|
||||
private AllSummaryEntry(List<List<WalletForm>> walletFormsList) {
|
||||
super(null, "All Wallets", walletFormsList.stream().map(SummaryEntry::new).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getValue() {
|
||||
long value = 0;
|
||||
for(Entry entry : getChildren()) {
|
||||
value += entry.getValue();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEntryType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function getWalletFunction() {
|
||||
return Function.TRANSACTIONS;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SummaryEntry extends Entry {
|
||||
private SummaryEntry(List<WalletForm> walletForms) {
|
||||
super(walletForms.get(0).getWallet(), walletForms.get(0).getWallet().getName(), walletForms.stream().map(WalletForm::getWalletTransactionsEntry).collect(Collectors.toList()));
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WebcamMirroredChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.CheckMenuItem;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
@@ -21,12 +26,26 @@ public class WebcamView {
|
||||
|
||||
private final ObjectProperty<Image> imageProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
public WebcamView(WebcamService service) {
|
||||
public WebcamView(WebcamService service, boolean mirrored) {
|
||||
this.service = service ;
|
||||
this.imageView = new ImageView();
|
||||
imageView.setPreserveRatio(true);
|
||||
// make the cam behave like a mirror:
|
||||
imageView.setScaleX(-1);
|
||||
if(mirrored) {
|
||||
setMirrored(true);
|
||||
}
|
||||
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
CheckMenuItem mirrorItem = new CheckMenuItem("Mirror Camera");
|
||||
mirrorItem.setSelected(mirrored);
|
||||
mirrorItem.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setMirrored(newValue);
|
||||
Config.get().setMirrorCapture(newValue);
|
||||
EventManager.get().post(new WebcamMirroredChangedEvent(newValue));
|
||||
});
|
||||
contextMenu.getItems().add(mirrorItem);
|
||||
imageView.setOnContextMenuRequested(event -> {
|
||||
contextMenu.show(imageView, event.getScreenX(), event.getScreenY());
|
||||
});
|
||||
|
||||
service.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null) {
|
||||
@@ -112,4 +131,8 @@ public class WebcamView {
|
||||
public Node getView() {
|
||||
return view;
|
||||
}
|
||||
|
||||
public void setMirrored(boolean mirrored) {
|
||||
imageView.setScaleX(mirrored ? -1 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class KeystoreDeviceRegistrationsChangedEvent extends WalletChangedEvent {
|
||||
private final List<Keystore> changedKeystores;
|
||||
|
||||
public KeystoreDeviceRegistrationsChangedEvent(Wallet wallet, List<Keystore> changedKeystores) {
|
||||
super(wallet);
|
||||
this.changedKeystores = changedKeystores;
|
||||
}
|
||||
|
||||
public List<Keystore> getChangedKeystores() {
|
||||
return changedKeystores;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class SorobanInitiatedEvent {
|
||||
private Wallet wallet;
|
||||
|
||||
public SorobanInitiatedEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
@@ -17,7 +16,6 @@ public class SpendUtxoEvent {
|
||||
private final Long fee;
|
||||
private final boolean requireAllUtxos;
|
||||
private final BlockTransaction replacedTransaction;
|
||||
private final Pool pool;
|
||||
private final PaymentCode paymentCode;
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
@@ -32,19 +30,6 @@ public class SpendUtxoEvent {
|
||||
this.fee = fee;
|
||||
this.requireAllUtxos = requireAllUtxos;
|
||||
this.replacedTransaction = replacedTransaction;
|
||||
this.pool = null;
|
||||
this.paymentCode = null;
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, Pool pool) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this.payments = payments;
|
||||
this.opReturns = opReturns;
|
||||
this.fee = fee;
|
||||
this.requireAllUtxos = false;
|
||||
this.replacedTransaction = null;
|
||||
this.pool = pool;
|
||||
this.paymentCode = null;
|
||||
}
|
||||
|
||||
@@ -56,7 +41,6 @@ public class SpendUtxoEvent {
|
||||
this.fee = null;
|
||||
this.requireAllUtxos = false;
|
||||
this.replacedTransaction = null;
|
||||
this.pool = null;
|
||||
this.paymentCode = paymentCode;
|
||||
}
|
||||
|
||||
@@ -88,10 +72,6 @@ public class SpendUtxoEvent {
|
||||
return replacedTransaction;
|
||||
}
|
||||
|
||||
public Pool getPool() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
public PaymentCode getPaymentCode() {
|
||||
return paymentCode;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.TableType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTable;
|
||||
|
||||
public class WalletTableChangedEvent extends WalletChangedEvent {
|
||||
private final WalletTable walletTable;
|
||||
|
||||
public WalletTableChangedEvent(Wallet wallet, WalletTable walletTable) {
|
||||
super(wallet);
|
||||
this.walletTable = walletTable;
|
||||
}
|
||||
|
||||
public WalletTable getWalletTable() {
|
||||
return walletTable;
|
||||
}
|
||||
|
||||
public TableType getTableType() {
|
||||
return walletTable == null ? null : walletTable.getTableType();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class WebcamMirroredChangedEvent {
|
||||
private final boolean mirrored;
|
||||
|
||||
public WebcamMirroredChangedEvent(boolean mirrored) {
|
||||
this.mirrored = mirrored;
|
||||
}
|
||||
|
||||
public boolean isMirrored() {
|
||||
return mirrored;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class WebcamResolutionChangedEvent {
|
||||
private final boolean hdResolution;
|
||||
|
||||
public WebcamResolutionChangedEvent(boolean hdResolution) {
|
||||
this.hdResolution = hdResolution;
|
||||
}
|
||||
|
||||
public boolean isHdResolution() {
|
||||
return hdResolution;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WhirlpoolIndexHighFrequencyEvent {
|
||||
private final Wallet wallet;
|
||||
|
||||
public WhirlpoolIndexHighFrequencyEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WhirlpoolMixEvent {
|
||||
private final Wallet wallet;
|
||||
private final BlockTransactionHashIndex utxo;
|
||||
private final MixProgress mixProgress;
|
||||
private final Utxo nextUtxo;
|
||||
private final MixFailReason mixFailReason;
|
||||
private final String mixError;
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixProgress mixProgress) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = mixProgress;
|
||||
this.nextUtxo = null;
|
||||
this.mixFailReason = null;
|
||||
this.mixError = null;
|
||||
}
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = null;
|
||||
this.nextUtxo = nextUtxo;
|
||||
this.mixFailReason = null;
|
||||
this.mixError = null;
|
||||
}
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixFailReason mixFailReason, String mixError) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = null;
|
||||
this.nextUtxo = null;
|
||||
this.mixFailReason = mixFailReason;
|
||||
this.mixError = mixError;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public BlockTransactionHashIndex getUtxo() {
|
||||
return utxo;
|
||||
}
|
||||
|
||||
public MixProgress getMixProgress() {
|
||||
return mixProgress;
|
||||
}
|
||||
|
||||
public Utxo getNextUtxo() {
|
||||
return nextUtxo;
|
||||
}
|
||||
|
||||
public MixFailReason getMixFailReason() {
|
||||
return mixFailReason;
|
||||
}
|
||||
|
||||
public String getMixError() {
|
||||
return mixError;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
|
||||
public class WhirlpoolMixSuccessEvent extends WhirlpoolMixEvent {
|
||||
private final WalletNode walletNode;
|
||||
|
||||
public WhirlpoolMixSuccessEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo, WalletNode walletNode) {
|
||||
super(wallet, utxo, nextUtxo);
|
||||
this.walletNode = walletNode;
|
||||
}
|
||||
|
||||
public WalletNode getWalletNode() {
|
||||
return walletNode;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||
FEATHER_ALT('\uf56b'),
|
||||
FILE_CSV('\uf6dd'),
|
||||
FILE_IMPORT('\uf56f'),
|
||||
FILE_EXPORT('\uf56e'),
|
||||
FILE_PDF('\uf1c1'),
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||
|
||||
@@ -183,6 +183,13 @@ public class GlyphUtils {
|
||||
return successGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getInvalidGlyph() {
|
||||
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
invalidGlyph.getStyleClass().add("failure");
|
||||
invalidGlyph.setFontSize(12);
|
||||
return invalidGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getWarningGlyph() {
|
||||
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
warningGlyph.getStyleClass().add("warn-icon");
|
||||
@@ -203,4 +210,11 @@ public class GlyphUtils {
|
||||
busyGlyph.setFontSize(12);
|
||||
return busyGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getDownArrowGlyph() {
|
||||
Glyph downGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||
downGlyph.getStyleClass().add("arrow-down");
|
||||
downGlyph.setFontSize(12);
|
||||
return downGlyph;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Unchained or Caravan Multisig";
|
||||
return "Unchained Caravan Multisig";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -56,11 +56,11 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
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";
|
||||
extKey.bip32Path = "m/0/0/0/0";
|
||||
}
|
||||
|
||||
try {
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path));
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path, true));
|
||||
} catch(NumberFormatException e) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, scriptType.getDefaultDerivationPath()));
|
||||
}
|
||||
@@ -107,7 +107,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
CaravanFile cf = new CaravanFile();
|
||||
cf.name = wallet.getFullName();
|
||||
cf.addressType = wallet.getScriptType().toString().replace('-', '_');
|
||||
cf.network = Network.get().getName();
|
||||
cf.network = Network.getCanonical().getName();
|
||||
cf.client = new Client();
|
||||
|
||||
Quorum quorum = new Quorum();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
@@ -13,7 +14,6 @@ import com.sparrowwallet.sparrow.io.ckcard.CkCardApi;
|
||||
import com.sparrowwallet.sparrow.io.satochip.SatoCardApi;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -171,7 +171,7 @@ public abstract class CardApi {
|
||||
}
|
||||
|
||||
private static void setLibrary() {
|
||||
if(!initialized && Platform.getCurrent() == Platform.UNIX) {
|
||||
if(!initialized && OsType.getCurrent() == OsType.UNIX) {
|
||||
for(File lib : LINUX_PCSC_LIBS) {
|
||||
if(lib.exists()) {
|
||||
System.setProperty("sun.security.smartcardio.library", lib.getAbsolutePath());
|
||||
|
||||
@@ -48,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(Locale.ROOT), "m/" + coboKeystore.AccountKeyPath));
|
||||
keystore.setKeyDerivation(new KeyDerivation(coboKeystore.MasterFingerprint.toLowerCase(Locale.ROOT), "m/" + coboKeystore.AccountKeyPath, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(coboKeystore.ExtPubKey));
|
||||
|
||||
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(coboKeystore.ExtPubKey);
|
||||
|
||||
@@ -7,10 +7,7 @@ import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -36,6 +33,31 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
inputStream.transferTo(baos);
|
||||
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray());
|
||||
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray());
|
||||
|
||||
Keystore keystore;
|
||||
try {
|
||||
keystore = getKeystoreMultisig(scriptType, firstClone, password);
|
||||
} catch(Exception e) {
|
||||
keystore = getKeystoreSinglesig(scriptType, secondClone, password);
|
||||
}
|
||||
|
||||
return keystore;
|
||||
} catch(IOException e) {
|
||||
throw new ImportException("Error importing keystore for " + scriptType, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Keystore getKeystoreSinglesig(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
ColdcardSinglesig coldcardSinglesig = new ColdcardSinglesig();
|
||||
return coldcardSinglesig.getKeystore(scriptType, inputStream, password);
|
||||
}
|
||||
|
||||
public Keystore getKeystoreMultisig(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||
ColdcardKeystore cck = JsonPersistence.getGson().fromJson(reader, ColdcardKeystore.class);
|
||||
|
||||
@@ -49,16 +71,16 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
if(header.getDefaultScriptType() != scriptType) {
|
||||
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
|
||||
}
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
|
||||
} else if(scriptType.equals(ScriptType.P2SH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
|
||||
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
|
||||
} else if(scriptType.equals(ScriptType.P2WSH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
|
||||
} else {
|
||||
throw new ImportException("Correct derivation not found for script type: " + scriptType);
|
||||
@@ -86,7 +108,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file created by using the Settings > Multisig Wallets > Export XPUB > " + account + " feature on your Coldcard.";
|
||||
return "Import file or QR created by using Advanced/Tools > Export Wallet > Sparrow Wallet" + (account > 0 ? " > 1 > " + account : "") + " on your Coldcard. For older firmware use Settings > Multisig Wallets > Export XPUB > " + account + ".";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -135,7 +157,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
Keystore keystore = new Keystore("Coldcard");
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COLDCARD);
|
||||
keystore.setKeyDerivation(new KeyDerivation(key, derivation));
|
||||
keystore.setKeyDerivation(new KeyDerivation(key, derivation, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(value));
|
||||
wallet.makeLabelsUnique(keystore);
|
||||
wallet.getKeystores().add(keystore);
|
||||
@@ -149,8 +171,10 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
wallet.setDefaultPolicy(policy);
|
||||
wallet.setScriptType(scriptType);
|
||||
|
||||
if(!wallet.isValid()) {
|
||||
throw new IllegalStateException("This file does not describe a valid wallet. " + getKeystoreImportDescription());
|
||||
try {
|
||||
wallet.checkWallet();
|
||||
} catch(InvalidWalletException e) {
|
||||
throw new IllegalStateException("This file does not describe a valid wallet: " + e.getMessage());
|
||||
}
|
||||
|
||||
return wallet;
|
||||
@@ -161,7 +185,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return "Import file created by using the Settings > Multisig Wallets > [Wallet Detail] > Coldcard Export feature on your Coldcard.";
|
||||
return "Import file or QR created by using Settings > Multisig Wallets > [Wallet Detail] > Coldcard Export on your Coldcard.";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -214,7 +238,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "Export file that can be read by your Coldcard using the Settings > Multisig Wallets > Import from File feature.";
|
||||
return "Export file or QR that can be read by your Coldcard using Settings > Multisig Wallets > Import from File or QR.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -30,7 +30,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
|
||||
@Override
|
||||
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.";
|
||||
return "Import file or QR created by using Advanced/Tools > Export Wallet > Sparrow Wallet" + (account > 0 ? " > 1 > " + account : "") + " on your Coldcard. For older firmware use Advanced > MicroSD > Export Wallet > Generic JSON > " + account + ".";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -78,7 +78,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COLDCARD);
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, ck.deriv));
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, ck.deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(ck.xpub));
|
||||
|
||||
return keystore;
|
||||
|
||||
@@ -54,10 +54,10 @@ public class Config {
|
||||
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 QRDensity qrDensity;
|
||||
private Boolean hdCapture;
|
||||
private boolean mirrorCapture = true;
|
||||
private boolean useZbar = true;
|
||||
private String webcamDevice;
|
||||
private ServerType serverType;
|
||||
@@ -77,7 +77,6 @@ public class Config {
|
||||
private int maxServerTimeout = DEFAULT_MAX_TIMEOUT;
|
||||
private int maxPageSize = DEFAULT_PAGE_SIZE;
|
||||
private boolean usePayNym;
|
||||
private boolean sameAppMixing;
|
||||
private boolean mempoolFullRbf;
|
||||
private Double appWidth;
|
||||
private Double appHeight;
|
||||
@@ -371,15 +370,6 @@ public class Config {
|
||||
return dustAttackThreshold;
|
||||
}
|
||||
|
||||
public File getHwi() {
|
||||
return hwi;
|
||||
}
|
||||
|
||||
public void setHwi(File hwi) {
|
||||
this.hwi = hwi;
|
||||
flush();
|
||||
}
|
||||
|
||||
public int getEnumerateHwPeriod() {
|
||||
return enumerateHwPeriod;
|
||||
}
|
||||
@@ -406,6 +396,15 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isMirrorCapture() {
|
||||
return mirrorCapture;
|
||||
}
|
||||
|
||||
public void setMirrorCapture(boolean mirrorCapture) {
|
||||
this.mirrorCapture = mirrorCapture;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isUseZbar() {
|
||||
return useZbar;
|
||||
}
|
||||
@@ -662,15 +661,6 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isSameAppMixing() {
|
||||
return sameAppMixing;
|
||||
}
|
||||
|
||||
public void setSameAppMixing(boolean sameAppMixing) {
|
||||
this.sameAppMixing = sameAppMixing;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isMempoolFullRbf() {
|
||||
return mempoolFullRbf;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.lark.HardwareClient;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -122,4 +123,18 @@ public class Device {
|
||||
public int hashCode() {
|
||||
return Objects.hash(type, path);
|
||||
}
|
||||
|
||||
public static Device fromHardwareClient(HardwareClient hardwareClient) {
|
||||
Device device = new Device();
|
||||
device.type = hardwareClient.getType();
|
||||
device.path = hardwareClient.getPath();
|
||||
device.model = hardwareClient.getModel();
|
||||
device.needsPinSent = hardwareClient.needsPinSent();
|
||||
device.needsPassphraseSent = hardwareClient.needsPassphraseSent();
|
||||
device.fingerprint = hardwareClient.fingerprint();
|
||||
device.card = hardwareClient.card();
|
||||
device.warnings = hardwareClient.warnings();
|
||||
device.error = hardwareClient.error();
|
||||
return device;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
keystore.setWalletModel(WalletModel.ELECTRUM);
|
||||
}
|
||||
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, derivationPath));
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, derivationPath, true));
|
||||
keystore.setExtendedPublicKey(xPub);
|
||||
keystore.setLabel(ek.label != null ? ek.label : "Electrum");
|
||||
if(keystore.getLabel().length() > Keystore.MAX_LABEL_LENGTH) {
|
||||
@@ -328,8 +328,12 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
ek.derivation = keystore.getKeyDerivation().getDerivationPath();
|
||||
ek.root_fingerprint = keystore.getKeyDerivation().getMasterFingerprint();
|
||||
ek.xpub = keystore.getExtendedPublicKey().toString(xpubHeader);
|
||||
ek.type = "hardware";
|
||||
ek.hw_type = keystore.getWalletModel().getType();
|
||||
if(keystore.getWalletModel().hasUsb()) {
|
||||
ek.type = "hardware";
|
||||
ek.hw_type = keystore.getWalletModel().getType();
|
||||
} else {
|
||||
ek.type = "bip32";
|
||||
}
|
||||
ew.use_encryption = false;
|
||||
} else if(keystore.getSource() == KeystoreSource.SW_SEED) {
|
||||
ek.type = "bip32";
|
||||
|
||||
@@ -36,7 +36,7 @@ public class GordianSeedTool implements KeystoreFileImport {
|
||||
|
||||
@Override
|
||||
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.";
|
||||
return "Select your seed and scan the QR code created by Authenticate > Derive Key > Other Key Derivations > " + Network.getCanonical().getCapitalizedName() + " > Master Key > Account Descriptor. Click the share icon at the bottom.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,48 +1,47 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.google.gson.*;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.lark.DeviceException;
|
||||
import com.sparrowwallet.lark.Lark;
|
||||
import com.sparrowwallet.lark.bitbox02.BitBoxFileNoiseConfig;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.control.BitBoxPairingDialog;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
||||
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import javax.smartcardio.CardNotPresentException;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
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.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
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 = "3.0.0";
|
||||
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
|
||||
private static final String LARK_HOME_DIR = "lark";
|
||||
private static final String BITBOX_FILENAME = "bitbox02.json";
|
||||
|
||||
private static boolean isPromptActive = false;
|
||||
|
||||
private final Set<byte[]> newDeviceRegistrations = new HashSet<>();
|
||||
|
||||
static {
|
||||
deleteHwiDir();
|
||||
}
|
||||
|
||||
public List<Device> enumerate(String passphrase) throws ImportException {
|
||||
List<Device> devices = new ArrayList<>();
|
||||
devices.addAll(enumerateUsb(passphrase));
|
||||
@@ -51,45 +50,13 @@ public class Hwi {
|
||||
}
|
||||
|
||||
private List<Device> enumerateUsb(String passphrase) throws ImportException {
|
||||
String output = null;
|
||||
try {
|
||||
List<String> command;
|
||||
if(passphrase != null) {
|
||||
command = new ArrayList<>(List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", escape(passphrase), Command.ENUMERATE.toString()));
|
||||
} else {
|
||||
command = new ArrayList<>(List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString()));
|
||||
}
|
||||
|
||||
addChainType(command, true);
|
||||
|
||||
Lark lark = getLark(passphrase);
|
||||
isPromptActive = true;
|
||||
output = execute(command);
|
||||
Device[] devices = getGson().fromJson(output, Device[].class);
|
||||
if(devices == null) {
|
||||
throw new ImportException("Error scanning, check devices are ready");
|
||||
}
|
||||
//Restore previous (pre v2.2.0) behaviour for Trezor One - don't default to an empty passphrase if one is not supplied
|
||||
Arrays.stream(devices).filter(device -> device.containsWarning("Using default passphrase of the empty string")).forEach(device -> {
|
||||
device.setFingerprint(null);
|
||||
device.setNeedsPassphraseSent(true);
|
||||
device.setError("Passphrase needs to be specified before the fingerprint information can be retrieved");
|
||||
});
|
||||
return Arrays.stream(devices).filter(device -> device != null && device.getModel() != null).collect(Collectors.toList());
|
||||
} catch(IOException e) {
|
||||
log.error("Error executing " + HWI_VERSION_DIR, e);
|
||||
throw new ImportException("Error executing HWI", e);
|
||||
} catch(Exception e) {
|
||||
if(output != null) {
|
||||
try {
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
JsonElement error = result.get("error");
|
||||
throw new ImportException(error.getAsString());
|
||||
} catch(Exception ex) {
|
||||
log.error("Error parsing JSON: " + output, e);
|
||||
throw new ImportException("Error scanning" + (output.isEmpty() ? ", check devices are ready" : ": " + output), e);
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
return lark.enumerate().stream().map(Device::fromHardwareClient).toList();
|
||||
} catch(Throwable e) {
|
||||
log.error("Error enumerating USB devices", e);
|
||||
throw new ImportException("Error scanning" + (e.getMessage() == null || e.getMessage().isEmpty() ? ", check devices are ready" : ": " + e.getMessage()), e);
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
@@ -124,31 +91,43 @@ public class Hwi {
|
||||
|
||||
public boolean promptPin(Device device) throws ImportException {
|
||||
try {
|
||||
String output = execute(getDeviceCommand(device, Command.PROMPT_PIN));
|
||||
Lark lark = getLark();
|
||||
boolean result = lark.promptPin(device.getType(), device.getPath());
|
||||
isPromptActive = true;
|
||||
return wasSuccessful(output);
|
||||
} catch(IOException e) {
|
||||
throw new ImportException(e);
|
||||
return result;
|
||||
} catch(DeviceException e) {
|
||||
throw new ImportException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error prompting pin", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean sendPin(Device device, String pin) throws ImportException {
|
||||
try {
|
||||
String output = execute(getDeviceCommand(device, Command.SEND_PIN, pin));
|
||||
Lark lark = getLark();
|
||||
boolean result = lark.sendPin(device.getType(), device.getPath(), pin);
|
||||
isPromptActive = false;
|
||||
return wasSuccessful(output);
|
||||
} catch(IOException e) {
|
||||
throw new ImportException(e);
|
||||
return result;
|
||||
} catch(DeviceException e) {
|
||||
throw new ImportException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error sending pin", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean togglePassphrase(Device device) throws ImportException {
|
||||
try {
|
||||
String output = execute(getDeviceCommand(device, Command.TOGGLE_PASSPHRASE));
|
||||
Lark lark = getLark();
|
||||
boolean result = lark.togglePassphrase(device.getType(), device.getPath());
|
||||
isPromptActive = false;
|
||||
return wasSuccessful(output);
|
||||
} catch(IOException e) {
|
||||
throw new ImportException(e);
|
||||
return result;
|
||||
} catch(DeviceException e) {
|
||||
throw new ImportException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error toggling passphrase", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,59 +142,36 @@ public class Hwi {
|
||||
|
||||
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
|
||||
try {
|
||||
String output;
|
||||
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceCommand(device, passphrase, Command.GET_XPUB, derivationPath));
|
||||
} else {
|
||||
output = execute(getDeviceCommand(device, Command.GET_XPUB, derivationPath));
|
||||
}
|
||||
|
||||
Lark lark = getLark(passphrase);
|
||||
ExtendedKey xpub = lark.getPubKeyAtPath(device.getType(), device.getPath(), derivationPath);
|
||||
isPromptActive = false;
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
if(result.get("xpub") != null) {
|
||||
return result.get("xpub").getAsString();
|
||||
} else {
|
||||
JsonElement error = result.get("error");
|
||||
if(error != null) {
|
||||
throw new ImportException(error.getAsString());
|
||||
} else {
|
||||
throw new ImportException("Could not retrieve xpub - reconnect your device and try again.");
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
throw new ImportException(e);
|
||||
return xpub.toString();
|
||||
} catch(DeviceException e) {
|
||||
throw new ImportException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error retrieving xpub", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) throws DisplayAddressException {
|
||||
public String displayAddress(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor,
|
||||
OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) throws DisplayAddressException {
|
||||
try {
|
||||
if(!Arrays.asList(ScriptType.ADDRESSABLE_TYPES).contains(scriptType)) {
|
||||
throw new IllegalArgumentException("Cannot display address for script type " + scriptType + ": Only addressable types supported");
|
||||
}
|
||||
|
||||
String descriptor = outputDescriptor.toString();
|
||||
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceCommand(device, passphrase, Command.DISPLAY_ADDRESS, "--desc", descriptor));
|
||||
} else {
|
||||
output = execute(getDeviceCommand(device, Command.DISPLAY_ADDRESS, "--desc", descriptor));
|
||||
}
|
||||
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
if(result.get("address") != null) {
|
||||
return result.get("address").getAsString();
|
||||
} else {
|
||||
JsonElement error = result.get("error");
|
||||
if(error != null) {
|
||||
throw new DisplayAddressException(error.getAsString());
|
||||
} else {
|
||||
throw new DisplayAddressException("Could not retrieve address");
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
throw new DisplayAddressException(e);
|
||||
Lark lark = getLark(passphrase, walletDescriptor, walletName, walletRegistration);
|
||||
String address = lark.displayAddress(device.getType(), device.getPath(), addressDescriptor);
|
||||
newDeviceRegistrations.addAll(lark.getWalletRegistrations().values());
|
||||
newDeviceRegistrations.remove(walletRegistration);
|
||||
return address;
|
||||
} catch(DeviceException e) {
|
||||
throw new DisplayAddressException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error displaying address", e);
|
||||
throw e;
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
@@ -224,406 +180,76 @@ public class Hwi {
|
||||
public String signMessage(Device device, String passphrase, String message, String derivationPath) throws SignMessageException {
|
||||
try {
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
|
||||
} else {
|
||||
output = execute(getDeviceArguments(device, Command.SIGN_MESSAGE), Command.SIGN_MESSAGE, message, derivationPath);
|
||||
}
|
||||
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
if(result.get("signature") != null) {
|
||||
return result.get("signature").getAsString();
|
||||
} else {
|
||||
JsonElement error = result.get("error");
|
||||
if(error != null) {
|
||||
throw new SignMessageException(error.getAsString());
|
||||
} else {
|
||||
throw new SignMessageException("Could not sign message");
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
throw new SignMessageException(e);
|
||||
Lark lark = getLark(passphrase);
|
||||
return lark.signMessage(device.getType(), device.getPath(), message, derivationPath);
|
||||
} catch(DeviceException e) {
|
||||
throw new SignMessageException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error signing message", e);
|
||||
throw e;
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
public PSBT signPSBT(Device device, String passphrase, PSBT psbt) throws SignTransactionException {
|
||||
public PSBT signPSBT(Device device, String passphrase, PSBT psbt,
|
||||
OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) throws SignTransactionException {
|
||||
try {
|
||||
String psbtBase64 = psbt.toBase64String();
|
||||
|
||||
isPromptActive = true;
|
||||
String output;
|
||||
if(passphrase != null && device.getModel().externalPassphraseEntry()) {
|
||||
output = execute(getDeviceArguments(device, passphrase, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
|
||||
} else {
|
||||
output = execute(getDeviceArguments(device, Command.SIGN_TX), Command.SIGN_TX, psbtBase64);
|
||||
}
|
||||
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
if(result.get("psbt") != null) {
|
||||
String strPsbt = result.get("psbt").getAsString();
|
||||
return PSBT.fromString(strPsbt);
|
||||
} else {
|
||||
JsonElement error = result.get("error");
|
||||
if(error != null && error.getAsString().equals("sign_tx canceled")) {
|
||||
throw new SignTransactionException("Signing cancelled");
|
||||
} else if(error != null && error.getAsString().equals("open failed")) {
|
||||
throw new SignTransactionException("Please reconnect your USB device");
|
||||
} else if(error != null) {
|
||||
throw new SignTransactionException(error.getAsString());
|
||||
} else {
|
||||
throw new SignTransactionException("Could not retrieve PSBT");
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
throw new SignTransactionException("Could not sign PSBT", e);
|
||||
} catch(PSBTParseException e) {
|
||||
throw new SignTransactionException("Could not parse signed PSBT" + (e.getMessage() != null ? ": " + e.getMessage() : ""), e);
|
||||
Lark lark = getLark(passphrase, walletDescriptor, walletName, walletRegistration);
|
||||
PSBT signed = lark.signTransaction(device.getType(), device.getPath(), psbt);
|
||||
newDeviceRegistrations.addAll(lark.getWalletRegistrations().values());
|
||||
newDeviceRegistrations.remove(walletRegistration);
|
||||
return signed;
|
||||
} catch(DeviceException e) {
|
||||
throw new SignTransactionException(e.getMessage(), e);
|
||||
} catch(RuntimeException e) {
|
||||
log.error("Error signing PSBT", e);
|
||||
throw e;
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
private String execute(List<String> command) throws IOException {
|
||||
long start = System.currentTimeMillis();
|
||||
Process process = null;
|
||||
try {
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||
process = processBuilder.start();
|
||||
return getProcessOutput(process);
|
||||
} finally {
|
||||
deleteExtractionOnFailure(process, start);
|
||||
}
|
||||
private Lark getLark() {
|
||||
return getLark(null);
|
||||
}
|
||||
|
||||
private String execute(List<String> arguments, Command command, String... commandArguments) throws IOException {
|
||||
long start = System.currentTimeMillis();
|
||||
Process process = null;
|
||||
try {
|
||||
List<String> processArguments = new ArrayList<>(arguments);
|
||||
private Lark getLark(String passphrase) {
|
||||
return getLark(passphrase, null, null, null);
|
||||
}
|
||||
|
||||
boolean useStdin = Arrays.stream(commandArguments).noneMatch(arg -> arg.contains("\n"));
|
||||
if(useStdin) {
|
||||
processArguments.add("--stdin");
|
||||
private Lark getLark(String passphrase, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
|
||||
Lark lark = new Lark(AppServices.getHttpClientService());
|
||||
lark.setBitBoxNoiseConfig(new BitBoxFxNoiseConfig());
|
||||
if(passphrase != null) {
|
||||
lark.setPassphrase(passphrase);
|
||||
}
|
||||
|
||||
if(walletDescriptor != null && walletName != null) {
|
||||
if(walletRegistration != null) {
|
||||
lark.addWalletRegistration(walletDescriptor, walletName, walletRegistration);
|
||||
} else {
|
||||
processArguments.add(command.toString());
|
||||
processArguments.addAll(Arrays.asList(commandArguments));
|
||||
lark.addWalletName(walletDescriptor, walletName);
|
||||
}
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(processArguments);
|
||||
process = processBuilder.start();
|
||||
|
||||
if(useStdin) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
return getProcessOutput(process);
|
||||
} finally {
|
||||
deleteExtractionOnFailure(process, start);
|
||||
}
|
||||
|
||||
return lark;
|
||||
}
|
||||
|
||||
private String getProcessOutput(Process process) throws IOException {
|
||||
String output;
|
||||
try(InputStreamReader reader = new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)) {
|
||||
output = CharStreams.toString(reader);
|
||||
}
|
||||
|
||||
if(output.isEmpty() && process.getErrorStream() != null) {
|
||||
try(InputStreamReader reader = new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8)) {
|
||||
String errorOutput = CharStreams.toString(reader);
|
||||
if(!errorOutput.isEmpty()) {
|
||||
throw new IOException(errorOutput);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private synchronized File getHwiExecutable(Command command) {
|
||||
File hwiExecutable = Config.get().getHwi();
|
||||
if(hwiExecutable != null && hwiExecutable.exists()) {
|
||||
File homeDir = getHwiHomeDir();
|
||||
String tmpDir = System.getProperty("java.io.tmpdir");
|
||||
String hwiPath = hwiExecutable.getAbsolutePath();
|
||||
if(command.isTestFirst() && (hwiPath.contains(tmpDir) || hwiPath.startsWith(homeDir.getAbsolutePath())) && (!hwiPath.contains(HWI_VERSION_DIR) || !testHwi(hwiExecutable))) {
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
IOUtils.deleteDirectory(hwiExecutable.getParentFile());
|
||||
} else {
|
||||
hwiExecutable.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(hwiExecutable == null || !hwiExecutable.exists()) {
|
||||
try {
|
||||
Platform platform = Platform.getCurrent();
|
||||
String osArch = System.getProperty("os.arch");
|
||||
Set<PosixFilePermission> ownerExecutableWritable = PosixFilePermissions.fromString("rwxr--r--");
|
||||
|
||||
//A PyInstaller --onefile expands into a new directory on every run triggering OSX Gatekeeper checks.
|
||||
//To avoid doing these with every invocation, use a --onedir packaging and expand into a temp folder on OSX
|
||||
//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;
|
||||
if(osArch.equals("aarch64")) {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/osx/aarch64/" + HWI_VERSION_DIR + "-mac-aarch64-signed.tar.bz2");
|
||||
} else {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.tar.bz2");
|
||||
}
|
||||
|
||||
if(inputStream == null) {
|
||||
throw new IllegalStateException("Cannot load " + HWI_VERSION_DIR + " from classpath");
|
||||
}
|
||||
|
||||
File hwiHomeDir = getHwiHomeDir();
|
||||
File hwiVersionDir = new File(hwiHomeDir, HWI_VERSION_DIR);
|
||||
IOUtils.deleteDirectory(hwiVersionDir);
|
||||
if(!hwiVersionDir.exists()) {
|
||||
Files.createDirectories(hwiVersionDir.toPath(), PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
}
|
||||
|
||||
log.debug("Using HWI path: " + hwiVersionDir.getAbsolutePath());
|
||||
|
||||
File hwiExec = null;
|
||||
try(TarArchiveInputStream tarInput = new TarArchiveInputStream(new BZip2CompressorInputStream(inputStream))) {
|
||||
TarArchiveEntry tarEntry = tarInput.getNextEntry();
|
||||
while(tarEntry != null) {
|
||||
if(tarEntry.isDirectory()) {
|
||||
newDirectory(hwiVersionDir, tarEntry, ownerExecutableWritable);
|
||||
} else if(tarEntry.isSymbolicLink()) {
|
||||
newSymlink(hwiVersionDir, tarEntry, ownerExecutableWritable);
|
||||
} else {
|
||||
File newFile = newFile(hwiVersionDir, tarEntry, ownerExecutableWritable);
|
||||
try(FileOutputStream fos = new FileOutputStream(newFile)) {
|
||||
ByteStreams.copy(tarInput, new FileOutputStream(newFile));
|
||||
fos.flush();
|
||||
}
|
||||
|
||||
if(tarEntry.getName().equals("hwi")) {
|
||||
hwiExec = newFile;
|
||||
}
|
||||
}
|
||||
|
||||
tarEntry = tarInput.getNextEntry();
|
||||
}
|
||||
}
|
||||
|
||||
hwiExecutable = hwiExec;
|
||||
} else {
|
||||
InputStream inputStream;
|
||||
Path tempExecPath;
|
||||
if(platform == Platform.WINDOWS) {
|
||||
Files.createDirectories(getHwiHomeDir().toPath());
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
|
||||
tempExecPath = Files.createTempFile(getHwiHomeDir().toPath(), HWI_VERSION_DIR, null);
|
||||
} else if(osArch.equals("aarch64")) {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/linux/aarch64/hwi");
|
||||
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
} else {
|
||||
inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
|
||||
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
|
||||
}
|
||||
|
||||
if(inputStream == null) {
|
||||
throw new IllegalStateException("Cannot load " + HWI_VERSION_DIR + " from classpath");
|
||||
}
|
||||
|
||||
File tempExec = tempExecPath.toFile();
|
||||
tempExec.deleteOnExit();
|
||||
try(OutputStream tempExecStream = new BufferedOutputStream(new FileOutputStream(tempExec))) {
|
||||
ByteStreams.copy(inputStream, tempExecStream);
|
||||
inputStream.close();
|
||||
tempExecStream.flush();
|
||||
};
|
||||
|
||||
hwiExecutable = tempExec;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error initializing HWI", e);
|
||||
}
|
||||
|
||||
Config.get().setHwi(hwiExecutable);
|
||||
}
|
||||
|
||||
return hwiExecutable;
|
||||
}
|
||||
|
||||
private File getHwiHomeDir() {
|
||||
if(Platform.getCurrent() == Platform.OSX || Platform.getCurrent() == Platform.WINDOWS) {
|
||||
return new File(Storage.getSparrowDir(), HWI_HOME_DIR);
|
||||
}
|
||||
|
||||
return new File(System.getProperty("java.io.tmpdir"));
|
||||
}
|
||||
|
||||
private boolean testHwi(File hwiExecutable) {
|
||||
private static void deleteHwiDir() {
|
||||
try {
|
||||
List<String> command = List.of(hwiExecutable.getAbsolutePath(), "--version");
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(command);
|
||||
Process process = processBuilder.start();
|
||||
int exitValue = process.waitFor();
|
||||
return exitValue == 0;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static File newDirectory(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
String destDirPath = destinationDir.getCanonicalPath();
|
||||
|
||||
Path path = Path.of(destDirPath, tarEntry.getName());
|
||||
File destDir = Files.createDirectory(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
|
||||
|
||||
String destSubDirPath = destDir.getCanonicalPath();
|
||||
if(!destSubDirPath.startsWith(destDirPath + File.separator)) {
|
||||
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
|
||||
}
|
||||
|
||||
return destDir;
|
||||
}
|
||||
|
||||
public static File newFile(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
String destDirPath = destinationDir.getCanonicalPath();
|
||||
|
||||
Path path = Path.of(destDirPath, tarEntry.getName());
|
||||
File destFile = Files.createFile(path, PosixFilePermissions.asFileAttribute(setFilePermissions)).toFile();
|
||||
|
||||
String destFilePath = destFile.getCanonicalPath();
|
||||
if(!destFilePath.startsWith(destDirPath + File.separator)) {
|
||||
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
|
||||
}
|
||||
|
||||
return destFile;
|
||||
}
|
||||
|
||||
public static File newSymlink(File destinationDir, TarArchiveEntry tarEntry, Set<PosixFilePermission> setFilePermissions) throws IOException {
|
||||
String destDirPath = destinationDir.getCanonicalPath();
|
||||
|
||||
Path path = Path.of(destDirPath, tarEntry.getName());
|
||||
File destFile = Files.createSymbolicLink(path, Paths.get(tarEntry.getLinkName())).toFile();
|
||||
|
||||
String destFilePath = destFile.getCanonicalPath();
|
||||
if(!destFilePath.startsWith(destDirPath + File.separator)) {
|
||||
throw new IOException("Entry is outside of the target dir: " + tarEntry.getName());
|
||||
}
|
||||
|
||||
return destFile;
|
||||
}
|
||||
|
||||
private boolean wasSuccessful(String output) throws ImportException {
|
||||
JsonObject result = JsonParser.parseString(output).getAsJsonObject();
|
||||
if(result.get("error") != null) {
|
||||
throw new ImportException(result.get("error").getAsString());
|
||||
}
|
||||
|
||||
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);
|
||||
if(OsType.getCurrent() == OsType.MACOS || OsType.getCurrent() == OsType.WINDOWS) {
|
||||
File hwiHomeDir = new File(Storage.getSparrowDir(), HWI_HOME_DIR);
|
||||
if(hwiHomeDir.exists()) {
|
||||
IOUtils.deleteDirectory(hwiHomeDir);
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error deleting temporary extraction", e);
|
||||
log.error("Error deleting hwi directory", 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);
|
||||
return elements;
|
||||
}
|
||||
|
||||
private List<String> getDeviceArguments(Device device, String passphrase, Command command) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase)));
|
||||
addChainType(elements, false);
|
||||
return elements;
|
||||
}
|
||||
|
||||
private List<String> getDeviceCommand(Device device, Command command) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
|
||||
addChainType(elements, true);
|
||||
return elements;
|
||||
}
|
||||
|
||||
private List<String> getDeviceCommand(Device device, Command command, String... commandData) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), command.toString()));
|
||||
addChainType(elements, true);
|
||||
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
return elements;
|
||||
}
|
||||
|
||||
private List<String> getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase), command.toString()));
|
||||
addChainType(elements, true);
|
||||
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
return elements;
|
||||
}
|
||||
|
||||
private void addChainType(List<String> elements, boolean commandPresent) {
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
elements.add(elements.size() - (commandPresent ? 1 : 0), "--chain");
|
||||
elements.add(elements.size() - (commandPresent ? 1 : 0), getChainName(Network.get()));
|
||||
}
|
||||
}
|
||||
|
||||
private String getChainName(Network network) {
|
||||
if(network == Network.MAINNET) {
|
||||
return "main";
|
||||
} else if(network == Network.TESTNET) {
|
||||
return "test";
|
||||
}
|
||||
|
||||
return network.toString();
|
||||
}
|
||||
|
||||
private String escape(String passphrase) {
|
||||
Platform platform = Platform.getCurrent();
|
||||
if(platform == Platform.WINDOWS) {
|
||||
return passphrase.replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
public static class EnumerateService extends Service<List<Device>> {
|
||||
private final String passphrase;
|
||||
|
||||
@@ -724,13 +350,24 @@ public class Hwi {
|
||||
private final Device device;
|
||||
private final String passphrase;
|
||||
private final ScriptType scriptType;
|
||||
private final OutputDescriptor outputDescriptor;
|
||||
private final OutputDescriptor addressDescriptor;
|
||||
private final OutputDescriptor walletDescriptor;
|
||||
private final String walletName;
|
||||
private final byte[] walletRegistration;
|
||||
private final Set<byte[]> newDeviceRegistrations = new HashSet<>();
|
||||
|
||||
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor outputDescriptor) {
|
||||
public DisplayAddressService(Device device, String passphrase, ScriptType scriptType, OutputDescriptor addressDescriptor, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
|
||||
this.device = device;
|
||||
this.passphrase = passphrase;
|
||||
this.scriptType = scriptType;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
this.addressDescriptor = addressDescriptor;
|
||||
this.walletDescriptor = walletDescriptor;
|
||||
this.walletName = walletName;
|
||||
this.walletRegistration = walletRegistration;
|
||||
}
|
||||
|
||||
public Set<byte[]> getNewDeviceRegistrations() {
|
||||
return newDeviceRegistrations;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -738,7 +375,9 @@ public class Hwi {
|
||||
return new Task<>() {
|
||||
protected String call() throws DisplayAddressException {
|
||||
Hwi hwi = new Hwi();
|
||||
return hwi.displayAddress(device, passphrase, scriptType, outputDescriptor);
|
||||
String address = hwi.displayAddress(device, passphrase, scriptType, addressDescriptor, walletDescriptor, walletName, walletRegistration);
|
||||
newDeviceRegistrations.addAll(hwi.newDeviceRegistrations);
|
||||
return address;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -816,11 +455,22 @@ public class Hwi {
|
||||
private final Device device;
|
||||
private final String passphrase;
|
||||
private final PSBT psbt;
|
||||
private final OutputDescriptor walletDescriptor;
|
||||
private final String walletName;
|
||||
private final byte[] walletRegistration;
|
||||
private final Set<byte[]> newDeviceRegistrations = new HashSet<>();
|
||||
|
||||
public SignPSBTService(Device device, String passphrase, PSBT psbt) {
|
||||
public SignPSBTService(Device device, String passphrase, PSBT psbt, OutputDescriptor walletDescriptor, String walletName, byte[] walletRegistration) {
|
||||
this.device = device;
|
||||
this.passphrase = passphrase;
|
||||
this.psbt = psbt;
|
||||
this.walletDescriptor = walletDescriptor;
|
||||
this.walletName = walletName;
|
||||
this.walletRegistration = walletRegistration;
|
||||
}
|
||||
|
||||
public Set<byte[]> getNewDeviceRegistrations() {
|
||||
return newDeviceRegistrations;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -828,72 +478,61 @@ public class Hwi {
|
||||
return new Task<>() {
|
||||
protected PSBT call() throws SignTransactionException {
|
||||
Hwi hwi = new Hwi();
|
||||
return hwi.signPSBT(device, passphrase, psbt);
|
||||
PSBT signed = hwi.signPSBT(device, passphrase, psbt, walletDescriptor, walletName, walletRegistration);
|
||||
newDeviceRegistrations.addAll(hwi.newDeviceRegistrations);
|
||||
return signed;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public Gson getGson() {
|
||||
GsonBuilder gsonBuilder = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES);
|
||||
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelSerializer());
|
||||
gsonBuilder.registerTypeAdapter(WalletModel.class, new DeviceModelDeserializer());
|
||||
return gsonBuilder.create();
|
||||
}
|
||||
private static final class BitBoxFxNoiseConfig extends BitBoxFileNoiseConfig {
|
||||
private BitBoxPairingDialog pairingDialog;
|
||||
|
||||
private static class DeviceModelSerializer implements JsonSerializer<WalletModel> {
|
||||
@Override
|
||||
public JsonElement serialize(WalletModel src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return new JsonPrimitive(src.toString().toLowerCase(Locale.ROOT));
|
||||
public BitBoxFxNoiseConfig() {
|
||||
super(Path.of(Storage.getSparrowHome().getAbsolutePath(), LARK_HOME_DIR, BITBOX_FILENAME).toFile());
|
||||
}
|
||||
}
|
||||
|
||||
private static class DeviceModelDeserializer implements JsonDeserializer<WalletModel> {
|
||||
@Override
|
||||
public WalletModel deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
String modelStr = json.getAsJsonPrimitive().getAsString();
|
||||
try {
|
||||
return WalletModel.valueOf(modelStr.toUpperCase(Locale.ROOT).replace(' ', '_'));
|
||||
} catch(Exception e) {
|
||||
for(WalletModel model : WalletModel.values()) {
|
||||
if(modelStr.startsWith(model.getType())) {
|
||||
return model;
|
||||
}
|
||||
public boolean showPairing(String code, DeviceResponse response) throws DeviceException {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
AtomicBoolean confirmedDevice = new AtomicBoolean(false);
|
||||
|
||||
Thread showPairingDeviceThread = new Thread(() -> {
|
||||
try {
|
||||
isPromptActive = true;
|
||||
confirmedDevice.set(response.call());
|
||||
latch.countDown();
|
||||
} catch(DeviceException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
isPromptActive = false;
|
||||
}
|
||||
});
|
||||
showPairingDeviceThread.start();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
pairingDialog = new BitBoxPairingDialog(code);
|
||||
pairingDialog.initOwner(AppServices.getActiveWindow());
|
||||
pairingDialog.show();
|
||||
});
|
||||
|
||||
try {
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
throw new IllegalArgumentException("Could not determine wallet model for " + modelStr);
|
||||
}
|
||||
}
|
||||
|
||||
private enum Command {
|
||||
ENUMERATE("enumerate", true),
|
||||
PROMPT_PIN("promptpin", true),
|
||||
SEND_PIN("sendpin", false),
|
||||
TOGGLE_PASSPHRASE("togglepassphrase", true),
|
||||
DISPLAY_ADDRESS("displayaddress", true),
|
||||
SIGN_MESSAGE("signmessage", true),
|
||||
GET_XPUB("getxpub", true),
|
||||
SIGN_TX("signtx", true);
|
||||
Platform.runLater(() -> {
|
||||
if(pairingDialog != null && pairingDialog.isShowing()) {
|
||||
pairingDialog.setResult(ButtonType.APPLY);
|
||||
}
|
||||
if(!confirmedDevice.get()) {
|
||||
AppServices.showWarningDialog("Pairing Refused", "Pairing was refused on the device.");
|
||||
}
|
||||
});
|
||||
|
||||
private final String command;
|
||||
private final boolean testFirst;
|
||||
|
||||
Command(String command, boolean testFirst) {
|
||||
this.command = command;
|
||||
this.testFirst = testFirst;
|
||||
}
|
||||
|
||||
public String getCommand() {
|
||||
return command;
|
||||
}
|
||||
|
||||
public boolean isTestFirst() {
|
||||
return testFirst;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return command;
|
||||
return confirmedDevice.get();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public class Jade implements KeystoreFileImport {
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import QR created on your Jade by selecting Xpub Export from the Settings menu once you have loaded your seed.";
|
||||
return "Import QR created on your Jade by selecting Options > Wallet > Export Xpub once you have loaded your seed. Make sure to select Singlesig as the Wallet type in the Options menu there.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
@@ -67,4 +68,24 @@ public class JadeMultisig extends ColdcardMultisig {
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
throw new ImportException("Failed to detect a valid " + scriptType.getDescription() + " keystore.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import QR created on your Jade by selecting Options > Wallet > Export Xpub once you have loaded your seed. Make sure to select Multisig as the Wallet type in the Options menu there.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFileFormatAvailable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface KeystoreMnemonicShareImport extends KeystoreImport {
|
||||
Keystore getKeystore(List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
import java.io.InputStream;
|
||||
@@ -45,4 +46,25 @@ public class PassportMultisig extends ColdcardMultisig {
|
||||
public boolean isWalletExportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return "Import file or QR created by using Settings > Bitcoin > Multisig > [Wallet Detail] > Export via QR/microSD on your Passport.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
Wallet wallet = super.importWallet(inputStream, password);
|
||||
for(Keystore keystore : wallet.getKeystores()) {
|
||||
keystore.setLabel(keystore.getLabel().replace("Coldcard", "Passport"));
|
||||
keystore.setWalletModel(WalletModel.PASSPORT);
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user