mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4e9ef989d | ||
|
|
4ed8550f1d | ||
|
|
4bec71e7c4 | ||
|
|
961fd94dd6 | ||
|
|
7915bbfa47 | ||
|
|
49e70e8e9b | ||
|
|
6063b02113 | ||
|
|
faa5a11c94 | ||
|
|
acab50cdcd | ||
|
|
4d7d897e06 | ||
|
|
af532e7fc9 | ||
|
|
fd2b383dbc | ||
|
|
98b33e184e | ||
|
|
3bc7c7473a | ||
|
|
4f6981b869 | ||
|
|
258d46a253 | ||
|
|
40a3eb5d4f | ||
|
|
35965235f3 | ||
|
|
368b24ea3b | ||
|
|
107b5ba36c | ||
|
|
84978a3d5d | ||
|
|
dd3b980c36 | ||
|
|
b9a553abf2 | ||
|
|
48b3dbc353 | ||
|
|
fb40d991bb | ||
|
|
5fe6a7196a | ||
|
|
f06b859c82 | ||
|
|
80dd59928e | ||
|
|
f22f76464a | ||
|
|
dfe1f16495 | ||
|
|
90a9030ecb | ||
|
|
4ab33a373c | ||
|
|
10e751d6e1 | ||
|
|
5f40669af7 | ||
|
|
97b4ed48db | ||
|
|
9fc096569a | ||
|
|
41636f7152 | ||
|
|
fc5d48de6f | ||
|
|
2a7f14a4ed | ||
|
|
1cb6778502 | ||
|
|
7f254e763d | ||
|
|
e0ff42b6a4 | ||
|
|
88fc8f5017 | ||
|
|
d7072928de | ||
|
|
0cc9ddba05 | ||
|
|
e3799cd0a8 | ||
|
|
ea6b30326e | ||
|
|
38768885e2 | ||
|
|
c360177c31 | ||
|
|
e88ea0bac1 | ||
|
|
a66b36c59c | ||
|
|
fb3b674b65 | ||
|
|
eff0e201f3 | ||
|
|
58d10cbba4 | ||
|
|
29bce8a9bc | ||
|
|
67dcf69a78 | ||
|
|
7ad8a04bda | ||
|
|
24e75603c6 | ||
|
|
545342dfb4 | ||
|
|
b15d6308bd | ||
|
|
ff0c381437 | ||
|
|
555260e954 | ||
|
|
8d584d1c48 | ||
|
|
967cf0cdfa | ||
|
|
41ba8455a0 | ||
|
|
d84f3bf887 | ||
|
|
153815d9e3 | ||
|
|
0250579445 | ||
|
|
7590d786b5 | ||
|
|
55809b7dc3 | ||
|
|
06026b0a09 | ||
|
|
2cd64aa650 | ||
|
|
0b980f6ab5 | ||
|
|
73dcef9fd1 | ||
|
|
4e3491ec64 | ||
|
|
176e440195 | ||
|
|
300545b289 | ||
|
|
057a9efb1f | ||
|
|
9edeff9aab | ||
|
|
f938506a3f | ||
|
|
4fb8c5a61b | ||
|
|
7a99c4a11a | ||
|
|
6c13504644 | ||
|
|
3ddf4ed4b2 | ||
|
|
6b59ff60ad | ||
|
|
7c64d689fd | ||
|
|
4ad9cdedb6 | ||
|
|
276cb8aecb | ||
|
|
e7ed82699c | ||
|
|
68cd3673af | ||
|
|
5f96570c07 | ||
|
|
5147ee8aee | ||
|
|
3cc2981b72 | ||
|
|
8038298485 | ||
|
|
d1a1bd5751 | ||
|
|
63b7aef91e | ||
|
|
8a51a47156 | ||
|
|
827efe071e | ||
|
|
56784b684a | ||
|
|
1fa52f043c | ||
|
|
ce44cfe877 | ||
|
|
8ba0a9f360 | ||
|
|
41dabac75b | ||
|
|
064708f088 | ||
|
|
66dc394215 | ||
|
|
5ca60699ef | ||
|
|
61d9ad1875 | ||
|
|
af6bbebac4 | ||
|
|
6f4fc4f2ca | ||
|
|
00f5001385 | ||
|
|
3f3cdca94f | ||
|
|
08cf01a5c6 | ||
|
|
df7f40dbc9 | ||
|
|
12c1725260 | ||
|
|
aa8380eb03 | ||
|
|
0e26f8fce1 | ||
|
|
8de14dcbce | ||
|
|
6871810c7c | ||
|
|
6ac294920e | ||
|
|
4b32eb397e | ||
|
|
b25297e8b9 | ||
|
|
ff90a2c3e6 | ||
|
|
3cbe8d1537 | ||
|
|
9293b622a3 | ||
|
|
6337e1cf7d | ||
|
|
3ff3fb29b0 | ||
|
|
149d297193 | ||
|
|
47f7b8870c | ||
|
|
7ce7d37da7 | ||
|
|
b422c754d6 | ||
|
|
be6e9019dc | ||
|
|
011bb86b5f | ||
|
|
8e1163d3db | ||
|
|
83c8b1c8e6 | ||
|
|
d44aecea90 | ||
|
|
c9288ab25b | ||
|
|
e39a2cb944 | ||
|
|
fb25edb51c | ||
|
|
06ff0498d4 | ||
|
|
63b27e7054 | ||
|
|
0260a12663 | ||
|
|
a05fcba6d9 | ||
|
|
5be5363f25 | ||
|
|
cc961b4eeb | ||
|
|
7e7795196c | ||
|
|
fd0fe1110d | ||
|
|
ea64fa0f85 | ||
|
|
2972f1a4d7 | ||
|
|
6990b398c2 | ||
|
|
a25b53bd44 | ||
|
|
871c503bc9 | ||
|
|
b7992ae9e1 | ||
|
|
0a8eb2fbb7 | ||
|
|
7863fb7632 | ||
|
|
6481d83b0c | ||
|
|
402d9b14ec | ||
|
|
feeb4f0ac0 | ||
|
|
143eb3213e | ||
|
|
cd96fc1daa | ||
|
|
73d9cd2e68 | ||
|
|
3faf817148 | ||
|
|
96c88b7472 | ||
|
|
e2795c7ef3 | ||
|
|
e7f6f7f3db | ||
|
|
dd9868c918 | ||
|
|
c2d3afae59 | ||
|
|
04a516d56b | ||
|
|
b27709e96f | ||
|
|
ebb7d23a05 | ||
|
|
97d121244f | ||
|
|
60dbc8ed84 | ||
|
|
29cd321724 | ||
|
|
467b834955 | ||
|
|
3bfa5d3dc4 | ||
|
|
f501f08e17 | ||
|
|
7d796369d6 | ||
|
|
c572011578 | ||
|
|
dbc1e7746b | ||
|
|
9325a1968b | ||
|
|
85166635b4 | ||
|
|
ed69a86529 | ||
|
|
ab2c77695b | ||
|
|
6ad81e1228 | ||
|
|
ff340c2449 | ||
|
|
f2b0f8ca9e | ||
|
|
0c213294ad | ||
|
|
7cdb7319ee | ||
|
|
d1ff8d6e3e | ||
|
|
d9ddc74d73 | ||
|
|
603df6d0f6 | ||
|
|
cbf847a57f | ||
|
|
e8fb676a24 | ||
|
|
273f3043fb | ||
|
|
8f165b05c7 | ||
|
|
8eb092a8d6 | ||
|
|
8dd1850905 | ||
|
|
0fa6bd56e2 | ||
|
|
c4c581525a | ||
|
|
d6a3824690 | ||
|
|
7dba141073 | ||
|
|
f2f6e639dc | ||
|
|
78afc5e4d5 | ||
|
|
b2d85b6c78 | ||
|
|
f3b0d37c54 | ||
|
|
6768ad2028 | ||
|
|
81cde4756a | ||
|
|
778564a954 | ||
|
|
1e4c8c3837 | ||
|
|
22408103ea | ||
|
|
383d3954d8 | ||
|
|
77a4e4aa50 | ||
|
|
19dedfa070 | ||
|
|
52696b014f | ||
|
|
8fb6de85f1 | ||
|
|
8270eb71db | ||
|
|
edcf12de5a | ||
|
|
da3399468c | ||
|
|
923c61fceb | ||
|
|
d3d939889e | ||
|
|
2b4d3fac6c | ||
|
|
1f67692727 | ||
|
|
bacbdb848b | ||
|
|
51ba7fc4cf | ||
|
|
59efed9e42 | ||
|
|
193fea4b69 | ||
|
|
b5cc2ff4c3 | ||
|
|
252ec58065 | ||
|
|
b8979ed8b0 | ||
|
|
c24f953e52 | ||
|
|
d139ca2706 | ||
|
|
bd421e877a | ||
|
|
25e1250710 | ||
|
|
fad1dad76e | ||
|
|
b3bd42b8f6 | ||
|
|
5aea538f09 | ||
|
|
16755e3140 | ||
|
|
cbfb7230a8 | ||
|
|
e438389953 | ||
|
|
6534ccb07e | ||
|
|
ca782dfc69 | ||
|
|
f1a662ba8a | ||
|
|
33fb2a38fc | ||
|
|
8b22e057bf |
@@ -7,7 +7,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
os: [windows-2022, ubuntu-20.04, macos-12]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
@@ -19,12 +19,6 @@ jobs:
|
||||
java-version: '18.0.1'
|
||||
- name: Show Build Versions
|
||||
run: ./gradlew -v
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
restore-keys: ${{ runner.os }}-gradle
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew jpackage
|
||||
- name: Package zip distribution
|
||||
@@ -34,9 +28,26 @@ jobs:
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew packageTarDistribution
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }}
|
||||
path: |
|
||||
build/jpackage/*
|
||||
!build/jpackage/Sparrow/
|
||||
- name: Headless build with Gradle
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew -Djava.awt.headless=true clean jpackage
|
||||
- name: Package headless tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
||||
- name: Rename Headless Artifacts
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done;
|
||||
- name: Upload Headless Artifact
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }} Headless
|
||||
path: |
|
||||
build/jpackage/*
|
||||
!build/jpackage/Sparrow/
|
||||
@@ -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 17 to be installed.
|
||||
The release binaries are built with [Eclipse Temurin 17.0.2+8](https://github.com/adoptium/temurin17-binaries/releases/tag/jdk-17.0.2%2B8).
|
||||
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).
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
|
||||
@@ -36,7 +36,7 @@ When updating to the latest HEAD
|
||||
|
||||
The release binaries are reproducible from v1.5.0 onwards (pre codesigning and installer packaging). More detailed [instructions on reproducing the binaries](docs/reproducible.md) are provided.
|
||||
|
||||
> A video documention of your built process uploaded to [bitcoinbinary.org](https://bitcoinbinary.org/) is appreciated. Alternatively check the site if you wish to see if someone else already verified the provided binaries.
|
||||
> Video documentation of your build process uploaded to [bitcoinbinary.org](https://bitcoinbinary.org/) is appreciated. Alternatively check the site if you wish to see if someone else already verified the provided binaries.
|
||||
|
||||
## Running
|
||||
|
||||
@@ -44,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
|
||||
|
||||
`./sparrow`
|
||||
|
||||
Java 17 or higher must be installed.
|
||||
Java 18 or higher must be installed.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -77,11 +77,11 @@ Note that if you are connecting to an Electrum server when using testnet, that s
|
||||
|
||||
When not explicitly configured using the command line argument above, Sparrow stores its mainnet config file, log file and wallets in a home folder location appropriate to the operating system:
|
||||
|
||||
Platform | Location
|
||||
-------- | --------
|
||||
OSX | ~/.sparrow
|
||||
Linux | ~/.sparrow
|
||||
Windows | %APPDATA%/Sparrow
|
||||
| Platform | Location |
|
||||
|----------| -------- |
|
||||
| OSX | ~/.sparrow |
|
||||
| Linux | ~/.sparrow |
|
||||
| Windows | %APPDATA%/Sparrow |
|
||||
|
||||
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
|
||||
|
||||
+204
-56
@@ -1,11 +1,13 @@
|
||||
import java.awt.GraphicsEnvironment
|
||||
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.13'
|
||||
id 'org-openjfx-javafxplugin'
|
||||
id 'extra-java-module-info'
|
||||
id 'org.beryx.jlink' version '2.25.0'
|
||||
id 'org.beryx.jlink' version '2.26.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.6.6'
|
||||
def sparrowVersion = '1.7.4'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -13,10 +15,26 @@ if(os.macOsX) {
|
||||
}
|
||||
def targetName = ""
|
||||
def osArch = "x64"
|
||||
def releaseArch = "x86_64"
|
||||
if(System.getProperty("os.arch") == "aarch64") {
|
||||
osArch = "aarch64"
|
||||
releaseArch = "aarch64"
|
||||
targetName = "-" + osArch
|
||||
}
|
||||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||
|
||||
def vTor = '4.7.13-4'
|
||||
def vKmpTor = '1.4.2'
|
||||
def kmpOs = osName
|
||||
if(os.macOsX) {
|
||||
kmpOs = "macos"
|
||||
} else if(os.windows) {
|
||||
kmpOs = "mingw"
|
||||
}
|
||||
def kmpArch = "x64"
|
||||
if(System.getProperty("os.arch") == "aarch64") {
|
||||
kmpArch = "arm64"
|
||||
}
|
||||
|
||||
group "com.sparrowwallet"
|
||||
version "${sparrowVersion}"
|
||||
@@ -49,7 +67,7 @@ dependencies {
|
||||
exclude group: 'org.hamcrest'
|
||||
exclude group: 'junit'
|
||||
}
|
||||
implementation('com.google.guava:guava:28.2-jre')
|
||||
implementation('com.google.guava:guava:31.1-jre')
|
||||
implementation('com.google.code.gson:gson:2.8.6')
|
||||
implementation('com.h2database:h2:2.1.214')
|
||||
implementation('com.zaxxer:HikariCP:4.0.3')
|
||||
@@ -64,11 +82,11 @@ dependencies {
|
||||
exclude group: 'com.beust', module: 'jcommander'
|
||||
}
|
||||
implementation('com.beust:jcommander:1.81')
|
||||
implementation('com.github.arteam:simple-json-rpc-core:1.0')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.0') {
|
||||
implementation('com.github.arteam:simple-json-rpc-core:1.3')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.3') {
|
||||
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
||||
}
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.0') {
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.4')
|
||||
@@ -79,10 +97,15 @@ dependencies {
|
||||
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
|
||||
exclude group: 'com.nativelibs4java', module: 'bridj'
|
||||
}
|
||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}${targetName}:0.6.8") {
|
||||
exclude group: 'org.jetbrains.kotlin'
|
||||
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('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20')
|
||||
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.6.4')
|
||||
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
@@ -98,18 +121,22 @@ dependencies {
|
||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.33')
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.36')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.27')
|
||||
testImplementation('junit:junit:4.12')
|
||||
implementation('com.github.librepdf:openpdf:1.3.30')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.1')
|
||||
implementation('net.coobird:thumbnailator:0.4.18')
|
||||
implementation('com.github.hervegirod:fxsvgimage:1.0b2')
|
||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||
testImplementation('junit:junit:4.13.1')
|
||||
}
|
||||
|
||||
application {
|
||||
mainModule = 'com.sparrowwallet.sparrow'
|
||||
mainClass = 'com.sparrowwallet.sparrow.MainApp'
|
||||
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
||||
}
|
||||
|
||||
compileJava {
|
||||
@@ -151,13 +178,19 @@ run {
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson"]
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core.jvm"]
|
||||
|
||||
if(os.macOsX) {
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
||||
}
|
||||
if(headless) {
|
||||
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||
}
|
||||
}
|
||||
|
||||
jlink {
|
||||
@@ -177,7 +210,7 @@ jlink {
|
||||
uses 'org.eclipse.jetty.websocket.common.RemoteEndpointFactory'
|
||||
}
|
||||
|
||||
options = ['--strip-debug', '--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', '2', '--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",
|
||||
@@ -195,9 +228,11 @@ jlink {
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.sql",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
|
||||
@@ -205,11 +240,18 @@ jlink {
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
|
||||
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor"]
|
||||
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core.jvm"]
|
||||
|
||||
if(os.windows) {
|
||||
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
|
||||
}
|
||||
if(os.macOsX) {
|
||||
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
|
||||
}
|
||||
if(headless) {
|
||||
jvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||
}
|
||||
}
|
||||
addExtraDependencies("javafx")
|
||||
jpackage {
|
||||
@@ -225,7 +267,12 @@ jlink {
|
||||
installerType = "exe"
|
||||
}
|
||||
if(os.linux) {
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow', '--linux-rpm-license-type', 'ASL 2.0']
|
||||
if(headless) {
|
||||
installerOptions = ['--license-file', 'LICENSE']
|
||||
} else {
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||
}
|
||||
installerOptions += ['--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
||||
}
|
||||
if(os.macOsX) {
|
||||
@@ -251,7 +298,7 @@ task packageZipDistribution(type: Zip) {
|
||||
|
||||
task packageTarDistribution(type: Tar) {
|
||||
dependsOn removeGroupWritePermission
|
||||
archiveFileName = "sparrow-${sparrowVersion}.tar.gz"
|
||||
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
compression = Compression.GZIP
|
||||
from("$buildDir/jpackage/") {
|
||||
@@ -260,7 +307,7 @@ task packageTarDistribution(type: Tar) {
|
||||
}
|
||||
|
||||
extraJavaModuleInfo {
|
||||
module('jackson-core-2.10.1.jar', 'com.fasterxml.jackson.core', '2.10.1') {
|
||||
module('jackson-core-2.13.2.jar', 'com.fasterxml.jackson.core', '2.13.2') {
|
||||
exports('com.fasterxml.jackson.core')
|
||||
exports('com.fasterxml.jackson.core.async')
|
||||
exports('com.fasterxml.jackson.core.base')
|
||||
@@ -275,11 +322,11 @@ extraJavaModuleInfo {
|
||||
exports('com.fasterxml.jackson.core.util')
|
||||
uses('com.fasterxml.jackson.core.ObjectCodec')
|
||||
}
|
||||
module('jackson-annotations-2.10.1.jar', 'com.fasterxml.jackson.annotation', '2.10.1') {
|
||||
module('jackson-annotations-2.13.2.jar', 'com.fasterxml.jackson.annotation', '2.13.2') {
|
||||
requires('com.fasterxml.jackson.core')
|
||||
exports('com.fasterxml.jackson.annotation')
|
||||
}
|
||||
module('jackson-databind-2.10.1.jar', 'com.fasterxml.jackson.databind', '2.10.1') {
|
||||
module('jackson-databind-2.13.2.jar', 'com.fasterxml.jackson.databind', '2.13.2') {
|
||||
requires('java.desktop')
|
||||
requires('java.logging')
|
||||
requires('com.fasterxml.jackson.annotation')
|
||||
@@ -313,7 +360,7 @@ extraJavaModuleInfo {
|
||||
exports('tornadofx.control')
|
||||
requires('javafx.controls')
|
||||
}
|
||||
module('simple-json-rpc-core-1.0.jar', 'simple.json.rpc.core', '1.0') {
|
||||
module('simple-json-rpc-core-1.3.jar', 'simple.json.rpc.core', '1.3') {
|
||||
exports('com.github.arteam.simplejsonrpc.core.annotation')
|
||||
exports('com.github.arteam.simplejsonrpc.core.domain')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
@@ -321,7 +368,7 @@ extraJavaModuleInfo {
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('org.jetbrains.annotations')
|
||||
}
|
||||
module('simple-json-rpc-client-1.0.jar', 'simple.json.rpc.client', '1.0') {
|
||||
module('simple-json-rpc-client-1.3.jar', 'simple.json.rpc.client', '1.3') {
|
||||
exports('com.github.arteam.simplejsonrpc.client')
|
||||
exports('com.github.arteam.simplejsonrpc.client.builder')
|
||||
exports('com.github.arteam.simplejsonrpc.client.exception')
|
||||
@@ -329,7 +376,7 @@ extraJavaModuleInfo {
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('simple.json.rpc.core')
|
||||
}
|
||||
module('simple-json-rpc-server-1.0.jar', 'simple.json.rpc.server', '1.0') {
|
||||
module('simple-json-rpc-server-1.3.jar', 'simple.json.rpc.server', '1.3') {
|
||||
exports('com.github.arteam.simplejsonrpc.server')
|
||||
requires('simple.json.rpc.core')
|
||||
requires('com.google.common')
|
||||
@@ -376,16 +423,11 @@ extraJavaModuleInfo {
|
||||
requires('java.xml')
|
||||
requires('java.logging')
|
||||
}
|
||||
module('kotlin-logging-1.5.4.jar', 'io.github.microutils.kotlin.logging', '1.5.4') {
|
||||
exports('mu')
|
||||
requires('kotlin.stdlib')
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('failureaccess-1.0.1.jar', 'failureaccess', '1.0.1') {
|
||||
exports('com.google.common.util.concurrent.internal')
|
||||
}
|
||||
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
|
||||
module('guava-28.2-jre.jar', 'com.google.common', '28.2-jre') {
|
||||
module('guava-31.1-jre.jar', 'com.google.common', '31.1-jre') {
|
||||
exports('com.google.common.eventbus')
|
||||
exports('com.google.common.net')
|
||||
exports('com.google.common.base')
|
||||
@@ -456,13 +498,6 @@ extraJavaModuleInfo {
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
|
||||
module('kotlin-stdlib-jdk8-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk8', '1.5.20')
|
||||
module('kotlin-stdlib-jdk7-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk7', '1.5.20')
|
||||
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
|
||||
exports('kotlin')
|
||||
exports('kotlin.jvm')
|
||||
exports('kotlin.collections')
|
||||
}
|
||||
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
|
||||
exports('com.sparrowwallet.hummingbird')
|
||||
exports('com.sparrowwallet.hummingbird.registry')
|
||||
@@ -470,8 +505,10 @@ extraJavaModuleInfo {
|
||||
}
|
||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||
exports('co.nstant.in.cbor')
|
||||
exports('co.nstant.in.cbor.model')
|
||||
exports('co.nstant.in.cbor.builder')
|
||||
}
|
||||
module('nightjar-0.2.33.jar', 'com.sparrowwallet.nightjar', '0.2.33') {
|
||||
module('nightjar-0.2.36.jar', 'com.sparrowwallet.nightjar', '0.2.36') {
|
||||
requires('com.google.common')
|
||||
requires('net.sourceforge.streamsupport')
|
||||
requires('org.slf4j')
|
||||
@@ -559,29 +596,140 @@ extraJavaModuleInfo {
|
||||
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
|
||||
exports('net.jcip.annotations')
|
||||
}
|
||||
module("netlayer-jpms-${osName}${targetName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
|
||||
exports('org.berndpruenster.netlayer.tor')
|
||||
requires('com.github.ravn.jsocks')
|
||||
requires('com.github.JesusMcCloud.jtorctl')
|
||||
module('thumbnailator-0.4.18.jar', 'net.coobird.thumbnailator', '0.4.18') {
|
||||
exports('net.coobird.thumbnailator')
|
||||
requires('java.desktop')
|
||||
}
|
||||
module('fxsvgimage-1.0b2.jar', 'com.github.hervegirod', '1.0b2') {
|
||||
exports('org.girod.javafx.svgimage')
|
||||
requires('javafx.graphics')
|
||||
requires('java.xml')
|
||||
}
|
||||
module('kotlinx-coroutines-core-jvm-1.6.4.jar', 'kotlinx.coroutines.core.jvm', '1.6.4') {
|
||||
exports('kotlinx.coroutines')
|
||||
requires('kotlin.stdlib')
|
||||
requires('commons.compress')
|
||||
requires('org.tukaani.xz')
|
||||
requires('java.instrument')
|
||||
uses('kotlinx.coroutines.CoroutineExceptionHandler')
|
||||
uses('kotlinx.coroutines.internal.MainDispatcherFactory')
|
||||
}
|
||||
module('kotlinx-coroutines-javafx-1.6.4.jar', 'kotlinx.coroutines.javafx', '1.6.4') {
|
||||
exports('kotlinx.coroutines.javafx')
|
||||
requires('kotlinx.coroutines.core.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module("kmp-tor-jvm-${vKmpTor}.jar", 'kmp.tor.jvm', "${vTor}-${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor')
|
||||
requires('kmp.tor.binary.extract.jvm')
|
||||
requires('kmp.tor.manager.jvm')
|
||||
requires('kmp.tor.manager.common.jvm')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.coroutines.core.jvm')
|
||||
requires('java.management')
|
||||
requires('io.github.microutils.kotlin.logging')
|
||||
}
|
||||
module('jtorctl-1.5.jar', 'com.github.JesusMcCloud.jtorctl', '1.5') {
|
||||
exports('net.freehaven.tor.control')
|
||||
if(kmpOs == "linux" && kmpArch == "arm64") {
|
||||
module("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") {
|
||||
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}") {
|
||||
exports("io.matthewnelson.kmp.tor.binary.${kmpOs}.${kmpArch}")
|
||||
exports("kmptor.${kmpOs}.${kmpArch}")
|
||||
}
|
||||
}
|
||||
module('commons-compress-1.18.jar', 'commons.compress', '1.18') {
|
||||
exports('org.apache.commons.compress')
|
||||
requires('org.tukaani.xz')
|
||||
module("kmp-tor-binary-extract-jvm-${vTor}.jar", 'kmp.tor.binary.extract.jvm', "${vTor}") {
|
||||
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('xz-1.6.jar', 'org.tukaani.xz', '1.6') {
|
||||
exports('org.tukaani.xz')
|
||||
module("kmp-tor-manager-jvm-${vKmpTor}.jar", 'kmp.tor.manager.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.manager')
|
||||
exports('io.matthewnelson.kmp.tor.manager.util')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kmp.tor.manager.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.coroutines.core.jvm')
|
||||
requires('kotlinx.atomicfu')
|
||||
requires('kmp.tor.controller.jvm')
|
||||
requires('kmp.tor.common.jvm')
|
||||
}
|
||||
module('jsocks-1.0.jar', 'com.github.ravn.jsocks', '1.0') {
|
||||
exports('com.runjva.sourceforge.jsocks.protocol')
|
||||
requires('org.slf4j')
|
||||
module("kmp-tor-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.manager.common.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.manager.common')
|
||||
exports('io.matthewnelson.kmp.tor.manager.common.event')
|
||||
exports('io.matthewnelson.kmp.tor.manager.common.state')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kmp.tor.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
}
|
||||
module("kmp-tor-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.controller.common.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.config')
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.file')
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.control')
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.control.usecase')
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.events')
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.exceptions')
|
||||
requires('kmp.tor.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.atomicfu')
|
||||
}
|
||||
module("kmp-tor-common-jvm-${vKmpTor}.jar", 'kmp.tor.common.jvm', "${vKmpTor}") {
|
||||
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}") {
|
||||
exports('io.matthewnelson.kmp.tor.controller.internal.controller')
|
||||
requires('kmp.tor.common.jvm')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kotlinx.coroutines.core.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.atomicfu')
|
||||
requires('encoding.core.jvm')
|
||||
requires('encoding.base16.jvm')
|
||||
}
|
||||
module("kmp-tor-ext-callback-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.common.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.common')
|
||||
}
|
||||
module("kmp-tor-ext-callback-manager-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.manager')
|
||||
requires('kmp.tor.manager.jvm')
|
||||
requires('kmp.tor.ext.callback.common.jvm')
|
||||
requires('kmp.tor.ext.callback.manager.common.jvm')
|
||||
requires('kmp.tor.ext.callback.controller.common.jvm')
|
||||
requires('kmp.tor.manager.common.jvm')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.coroutines.core.jvm')
|
||||
}
|
||||
module("kmp-tor-ext-callback-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.common.jvm', "${vKmpTor}") {
|
||||
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}") {
|
||||
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}") {
|
||||
exports('io.matthewnelson.kmp.tor.binary.geoip')
|
||||
exports('kmptor')
|
||||
}
|
||||
module("encoding-base16-jvm-1.2.1.jar", 'encoding.base16.jvm', "1.2.1") {
|
||||
exports('io.matthewnelson.encoding.base16')
|
||||
requires('encoding.core.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
}
|
||||
module("encoding-base32-jvm-1.2.1.jar", 'encoding.base32.jvm', "1.2.1")
|
||||
module("encoding-base64-jvm-1.2.1.jar", 'encoding.base64.jvm', "1.2.1")
|
||||
module("encoding-core-jvm-1.2.1.jar", 'encoding.core.jvm', "1.2.1") {
|
||||
exports('io.matthewnelson.encoding.core')
|
||||
requires('kotlin.stdlib')
|
||||
}
|
||||
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('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
|
||||
|
||||
@@ -4,10 +4,15 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
implementation 'org.ow2.asm:asm:8.0.1'
|
||||
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.0'
|
||||
implementation 'org.javamodularity:moduleplugin:1.8.12'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
}
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
@@ -17,5 +22,9 @@ gradlePlugin {
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright (c) 2018, 2020, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import org.gradle.api.GradleException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum JavaFXModule {
|
||||
|
||||
BASE,
|
||||
GRAPHICS(BASE),
|
||||
CONTROLS(BASE, GRAPHICS),
|
||||
FXML(BASE, GRAPHICS),
|
||||
MEDIA(BASE, GRAPHICS),
|
||||
SWING(BASE, GRAPHICS),
|
||||
WEB(BASE, CONTROLS, GRAPHICS, MEDIA);
|
||||
|
||||
static final String PREFIX_MODULE = "javafx.";
|
||||
private static final String PREFIX_ARTIFACT = "javafx-";
|
||||
|
||||
private List<JavaFXModule> dependentModules;
|
||||
|
||||
JavaFXModule(JavaFXModule...dependentModules) {
|
||||
this.dependentModules = List.of(dependentModules);
|
||||
}
|
||||
|
||||
public static Optional<JavaFXModule> fromModuleName(String moduleName) {
|
||||
return Stream.of(JavaFXModule.values())
|
||||
.filter(javaFXModule -> moduleName.equals(javaFXModule.getModuleName()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
public String getModuleName() {
|
||||
return PREFIX_MODULE + name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public String getModuleJarFileName() {
|
||||
return getModuleName() + ".jar";
|
||||
}
|
||||
|
||||
public String getArtifactName() {
|
||||
return PREFIX_ARTIFACT + name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public boolean compareJarFileName(JavaFXPlatform platform, String jarFileName) {
|
||||
Pattern p = Pattern.compile(getArtifactName() + "-.+-" + platform.getClassifier() + "\\.jar");
|
||||
return p.matcher(jarFileName).matches();
|
||||
}
|
||||
|
||||
public static Set<JavaFXModule> getJavaFXModules(List<String> moduleNames) {
|
||||
validateModules(moduleNames);
|
||||
|
||||
return moduleNames.stream()
|
||||
.map(JavaFXModule::fromModuleName)
|
||||
.flatMap(Optional::stream)
|
||||
.flatMap(javaFXModule -> javaFXModule.getMavenDependencies().stream())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static void validateModules(List<String> moduleNames) {
|
||||
var invalidModules = moduleNames.stream()
|
||||
.filter(module -> JavaFXModule.fromModuleName(module).isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (! invalidModules.isEmpty()) {
|
||||
throw new GradleException("Found one or more invalid JavaFX module names: " + invalidModules);
|
||||
}
|
||||
}
|
||||
|
||||
public List<JavaFXModule> getDependentModules() {
|
||||
return dependentModules;
|
||||
}
|
||||
|
||||
public List<JavaFXModule> getMavenDependencies() {
|
||||
List<JavaFXModule> dependencies = new ArrayList<>(dependentModules);
|
||||
dependencies.add(0, this);
|
||||
return dependencies;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (c) 2018, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.repositories.FlatDirectoryArtifactRepository;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.openjfx.gradle.JavaFXModule.PREFIX_MODULE;
|
||||
|
||||
public class JavaFXOptions {
|
||||
|
||||
private static final String MAVEN_JAVAFX_ARTIFACT_GROUP_ID = "org.openjfx";
|
||||
private static final String JAVAFX_SDK_LIB_FOLDER = "lib";
|
||||
|
||||
private final Project project;
|
||||
private final JavaFXPlatform platform;
|
||||
|
||||
private String version = "16";
|
||||
private String sdk;
|
||||
private String configuration = "implementation";
|
||||
private String lastUpdatedConfiguration;
|
||||
private List<String> modules = new ArrayList<>();
|
||||
private FlatDirectoryArtifactRepository customSDKArtifactRepository;
|
||||
|
||||
public JavaFXOptions(Project project) {
|
||||
this.project = project;
|
||||
this.platform = JavaFXPlatform.detect(project);
|
||||
}
|
||||
|
||||
public JavaFXPlatform getPlatform() {
|
||||
return platform;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
/**
|
||||
* If set, the JavaFX modules will be taken from this local
|
||||
* repository, and not from Maven Central
|
||||
* @param sdk, the path to the local JavaFX SDK folder
|
||||
*/
|
||||
public void setSdk(String sdk) {
|
||||
this.sdk = sdk;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
public String getSdk() {
|
||||
return sdk;
|
||||
}
|
||||
|
||||
/** Set the configuration name for dependencies, e.g.
|
||||
* 'implementation', 'compileOnly' etc.
|
||||
* @param configuration The configuration name for dependencies
|
||||
*/
|
||||
public void setConfiguration(String configuration) {
|
||||
this.configuration = configuration;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
public String getConfiguration() {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public List<String> getModules() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
public void setModules(List<String> modules) {
|
||||
this.modules = modules;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
public void modules(String...moduleNames) {
|
||||
setModules(List.of(moduleNames));
|
||||
}
|
||||
|
||||
private void updateJavaFXDependencies() {
|
||||
clearJavaFXDependencies();
|
||||
|
||||
String configuration = getConfiguration();
|
||||
JavaFXModule.getJavaFXModules(this.modules).stream()
|
||||
.sorted()
|
||||
.forEach(javaFXModule -> {
|
||||
if (customSDKArtifactRepository != null) {
|
||||
project.getDependencies().add(configuration, Map.of("name", javaFXModule.getModuleName()));
|
||||
} else {
|
||||
project.getDependencies().add(configuration,
|
||||
String.format("%s:%s:%s:%s", MAVEN_JAVAFX_ARTIFACT_GROUP_ID, javaFXModule.getArtifactName(),
|
||||
getVersion(), getPlatform().getClassifier()));
|
||||
}
|
||||
});
|
||||
lastUpdatedConfiguration = configuration;
|
||||
}
|
||||
|
||||
private void clearJavaFXDependencies() {
|
||||
if (customSDKArtifactRepository != null) {
|
||||
project.getRepositories().remove(customSDKArtifactRepository);
|
||||
customSDKArtifactRepository = null;
|
||||
}
|
||||
|
||||
if (sdk != null && ! sdk.isEmpty()) {
|
||||
Map<String, String> dirs = new HashMap<>();
|
||||
dirs.put("name", "customSDKArtifactRepository");
|
||||
if (sdk.endsWith(File.separator)) {
|
||||
dirs.put("dirs", sdk + JAVAFX_SDK_LIB_FOLDER);
|
||||
} else {
|
||||
dirs.put("dirs", sdk + File.separator + JAVAFX_SDK_LIB_FOLDER);
|
||||
}
|
||||
customSDKArtifactRepository = project.getRepositories().flatDir(dirs);
|
||||
}
|
||||
|
||||
if (lastUpdatedConfiguration == null) {
|
||||
return;
|
||||
}
|
||||
var configuration = project.getConfigurations().findByName(lastUpdatedConfiguration);
|
||||
if (configuration != null) {
|
||||
if (customSDKArtifactRepository != null) {
|
||||
configuration.getDependencies()
|
||||
.removeIf(dependency -> dependency.getName().startsWith(PREFIX_MODULE));
|
||||
}
|
||||
configuration.getDependencies()
|
||||
.removeIf(dependency -> MAVEN_JAVAFX_ARTIFACT_GROUP_ID.equals(dependency.getGroup()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (c) 2018, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import com.google.gradle.osdetector.OsDetector;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Project;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public enum JavaFXPlatform {
|
||||
|
||||
LINUX("linux", "linux-x86_64"),
|
||||
LINUX_MONOCLE("linux-monocle", "linux-x86_64-monocle"),
|
||||
LINUX_AARCH64("linux-aarch64", "linux-aarch_64"),
|
||||
LINUX_AARCH64_MONOCLE("linux-aarch64-monocle", "linux-aarch_64-monocle"),
|
||||
WINDOWS("win", "windows-x86_64"),
|
||||
WINDOWS_MONOCLE("win-monocle", "windows-x86_64-monocle"),
|
||||
OSX("mac", "osx-x86_64"),
|
||||
OSX_MONOCLE("mac-monocle", "osx-x86_64-monocle"),
|
||||
OSX_AARCH64("mac-aarch64", "osx-aarch_64"),
|
||||
OSX_AARCH64_MONOCLE("mac-aarch64-monocle", "osx-aarch_64-monocle");
|
||||
|
||||
private final String classifier;
|
||||
private final String osDetectorClassifier;
|
||||
|
||||
JavaFXPlatform( String classifier, String osDetectorClassifier ) {
|
||||
this.classifier = classifier;
|
||||
this.osDetectorClassifier = osDetectorClassifier;
|
||||
}
|
||||
|
||||
public String getClassifier() {
|
||||
return classifier;
|
||||
}
|
||||
|
||||
public static JavaFXPlatform detect(Project project) {
|
||||
|
||||
String osClassifier = project.getExtensions().getByType(OsDetector.class).getClassifier();
|
||||
|
||||
if("true".equals(System.getProperty("java.awt.headless"))) {
|
||||
osClassifier += "-monocle";
|
||||
}
|
||||
|
||||
for ( JavaFXPlatform platform: values()) {
|
||||
if ( platform.osDetectorClassifier.equals(osClassifier)) {
|
||||
return platform;
|
||||
}
|
||||
}
|
||||
|
||||
String supportedPlatforms = Arrays.stream(values())
|
||||
.map(p->p.osDetectorClassifier)
|
||||
.collect(Collectors.joining("', '", "'", "'"));
|
||||
|
||||
throw new GradleException(
|
||||
String.format(
|
||||
"Unsupported JavaFX platform found: '%s'! " +
|
||||
"This plugin is designed to work on supported platforms only." +
|
||||
"Current supported platforms are %s.", osClassifier, supportedPlatforms )
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2018, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import com.google.gradle.osdetector.OsDetectorPlugin;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.javamodularity.moduleplugin.ModuleSystemPlugin;
|
||||
import org.openjfx.gradle.tasks.ExecTask;
|
||||
|
||||
public class JavaFXPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().apply(OsDetectorPlugin.class);
|
||||
project.getPlugins().apply(ModuleSystemPlugin.class);
|
||||
|
||||
project.getExtensions().create("javafx", JavaFXOptions.class, project);
|
||||
|
||||
project.getTasks().create("configJavafxRun", ExecTask.class, project);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright (c) 2019, 2021, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle.tasks;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.logging.Logger;
|
||||
import org.gradle.api.logging.Logging;
|
||||
import org.gradle.api.plugins.ApplicationPlugin;
|
||||
import org.gradle.api.tasks.JavaExec;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.javamodularity.moduleplugin.extensions.RunModuleOptions;
|
||||
import org.openjfx.gradle.JavaFXModule;
|
||||
import org.openjfx.gradle.JavaFXOptions;
|
||||
import org.openjfx.gradle.JavaFXPlatform;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class ExecTask extends DefaultTask {
|
||||
|
||||
private static final Logger LOGGER = Logging.getLogger(ExecTask.class);
|
||||
|
||||
private final Project project;
|
||||
private JavaExec execTask;
|
||||
|
||||
@Inject
|
||||
public ExecTask(Project project) {
|
||||
this.project = project;
|
||||
project.getPluginManager().withPlugin(ApplicationPlugin.APPLICATION_PLUGIN_NAME, e -> {
|
||||
execTask = (JavaExec) project.getTasks().findByName(ApplicationPlugin.TASK_RUN_NAME);
|
||||
if (execTask != null) {
|
||||
execTask.dependsOn(this);
|
||||
} else {
|
||||
throw new GradleException("Run task not found.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void action() {
|
||||
if (execTask != null) {
|
||||
JavaFXOptions javaFXOptions = project.getExtensions().getByType(JavaFXOptions.class);
|
||||
JavaFXModule.validateModules(javaFXOptions.getModules());
|
||||
|
||||
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
|
||||
if (!definedJavaFXModuleNames.isEmpty()) {
|
||||
RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class);
|
||||
|
||||
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
|
||||
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
|
||||
);
|
||||
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
|
||||
|
||||
if (moduleOptions != null) {
|
||||
LOGGER.info("Modular JavaFX application found");
|
||||
// Remove empty JavaFX jars from classpath
|
||||
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
|
||||
definedJavaFXModuleNames.forEach(javaFXModule -> moduleOptions.getAddModules().add(javaFXModule));
|
||||
} else {
|
||||
LOGGER.info("Non-modular JavaFX application found");
|
||||
// Remove all JavaFX jars from classpath
|
||||
execTask.setClasspath(classpathWithoutJavaFXJars);
|
||||
|
||||
var javaFXModuleJvmArgs = List.of("--module-path", javaFXPlatformJars.getAsPath());
|
||||
|
||||
var jvmArgs = new ArrayList<String>();
|
||||
jvmArgs.add("--add-modules");
|
||||
jvmArgs.add(String.join(",", definedJavaFXModuleNames));
|
||||
|
||||
List<String> execJvmArgs = execTask.getJvmArgs();
|
||||
if (execJvmArgs != null) {
|
||||
jvmArgs.addAll(execJvmArgs);
|
||||
}
|
||||
jvmArgs.addAll(javaFXModuleJvmArgs);
|
||||
|
||||
execTask.setJvmArgs(jvmArgs);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isJavaFXJar(File jar, JavaFXPlatform platform) {
|
||||
return jar.isFile() &&
|
||||
Arrays.stream(JavaFXModule.values()).anyMatch(javaFXModule ->
|
||||
javaFXModule.compareJarFileName(platform, jar.getName()) ||
|
||||
javaFXModule.getModuleJarFileName().equals(jar.getName()));
|
||||
}
|
||||
}
|
||||
+83
-25
@@ -1,68 +1,125 @@
|
||||
## Reproducible builds
|
||||
# Reproducible builds
|
||||
|
||||
Reproducibility is a goal of the Sparrow Wallet project.
|
||||
Reproducibility is a goal of the Sparrow Wallet project.
|
||||
As of v1.5.0 and later, it is possible to recreate the exact binaries in the Github releases (specifically, the contents of the `.tar.gz` and `.zip` files).
|
||||
|
||||
Due to minor variances, it is not yet possible to reproduce the installer packages (`.deb`, `.rpm` and `.exe`).
|
||||
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
|
||||
In addition, the OSX binary is code signed and thus can't be directly reproduced yet.
|
||||
Work on resolving both of these issues is ongoing.
|
||||
|
||||
### Reproducing a release
|
||||
## Reproducing a release
|
||||
|
||||
#### Install Java
|
||||
### 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.4 and later, this is Eclipse Temurin 17.0.2+8.
|
||||
It is available for all supported platforms from [Eclipse Temurin 17.0.2+8](https://github.com/adoptium/temurin17-binaries/releases/tag/jdk-17.0.2%2B8).
|
||||
For v1.6.6 and later, this is Eclipse Temurin 18.0.1+10.
|
||||
|
||||
#### 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).
|
||||
|
||||
For reference, the downloads are as follows:
|
||||
- [Linux x64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_linux_hotspot_17.0.2_8.tar.gz)
|
||||
- [MacOS x64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_mac_hotspot_17.0.2_8.tar.gz)
|
||||
- [MacOS aarch64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_aarch64_mac_hotspot_17.0.2_8.tar.gz)
|
||||
- [Windows x64](https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.2%2B8/OpenJDK17U-jdk_x64_windows_hotspot_17.0.2_8.zip)
|
||||
- [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)
|
||||
|
||||
#### Java from Adoptium deb repo
|
||||
|
||||
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
|
||||
```shell
|
||||
sudo apt-get install -y wget apt-transport-https gnupg
|
||||
wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add -
|
||||
echo "deb https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
|
||||
|
||||
- Install dependencies:
|
||||
```sh
|
||||
sudo apt-get install -y wget curl apt-transport-https gnupg
|
||||
```
|
||||
|
||||
Download Adoptium public PGP key:
|
||||
```sh
|
||||
curl --tlsv1.2 --proto =https --location -o adoptium.asc https://packages.adoptium.net/artifactory/api/gpg/key/public
|
||||
```
|
||||
|
||||
Check if key fingerprint matches: `3B04D753C9050D9A5D343F39843C48A565F8F04B`:
|
||||
```
|
||||
gpg --import --import-options show-only adoptium.asc
|
||||
```
|
||||
If key doesn't match, do not procede.
|
||||
|
||||
Add Adoptium PGP key to a the keyring shared folder:
|
||||
```sh
|
||||
sudo cp adoptium.asc /usr/share/keyrings/
|
||||
```
|
||||
|
||||
Add Adoptium debian repository:
|
||||
```sh
|
||||
echo "deb [signed-by=/usr/share/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
|
||||
```
|
||||
|
||||
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-17-jdk=17.0.2+8
|
||||
sudo apt-get install -y temurin-18-jdk=18.0.1+10
|
||||
sudo update-alternatives --config java
|
||||
```
|
||||
|
||||
#### Java from SDK
|
||||
|
||||
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
|
||||
See the installation [instructions here](https://sdkman.io/install).
|
||||
Once installed, run
|
||||
```shell
|
||||
sdk install java 17.0.2-tem
|
||||
sdk install java 18.0.1-tem
|
||||
```
|
||||
|
||||
#### Other requirements
|
||||
### Other requirements
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
```shell
|
||||
sudo apt install -y rpm fakeroot binutils
|
||||
```
|
||||
|
||||
#### Building the binaries
|
||||
### Building the binaries
|
||||
|
||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||
|
||||
The project can cloned for a specific release tag as follows:
|
||||
```shell
|
||||
GIT_TAG="1.6.6"
|
||||
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
|
||||
GIT_TAG="1.7.3"
|
||||
```
|
||||
|
||||
Thereafter, building should be straightforward:
|
||||
The project can then be initially cloned as follows:
|
||||
|
||||
```shell
|
||||
cd sparrow
|
||||
git clone --recursive --branch "${GIT_TAG}" https://github.com/sparrowwallet/sparrow.git
|
||||
```
|
||||
|
||||
If you already have the sparrow repo cloned, fetch all new updates and checkout the release. For this, change into your local sparrow folder and execute:
|
||||
|
||||
```shell
|
||||
cd {yourPathToSparrow}/sparrow
|
||||
git pull --recurse-submodules
|
||||
git checkout "${GIT_TAG}"
|
||||
```
|
||||
|
||||
Note - there is an additional step if you updated rather than initially cloned your repo at `GIT_TAG`.
|
||||
This is due to the [drongo submodule](https://github.com/sparrowwallet/drongo/tree/master) which needs to be checked out to the commit state it had at the time of the release.
|
||||
Only then your build will be comparable to the provided one in the release section of Github.
|
||||
To checkout the submodule to the correct commit for `GIT_TAG`, additionally run:
|
||||
|
||||
```shell
|
||||
git submodule update --checkout
|
||||
```
|
||||
|
||||
Thereafter, building should be straightforward. If not already done, change into the sparrow folder and run:
|
||||
|
||||
```shell
|
||||
cd {yourPathToSparrow}/sparrow # if you aren't already in the sparrow folder
|
||||
./gradlew jpackage
|
||||
```
|
||||
|
||||
The binaries (and installers) will be placed in the `build/jpackage` folder.
|
||||
|
||||
#### Verifying the binaries are identical
|
||||
### Verifying the binaries are identical
|
||||
|
||||
Verify the built binaries against the released binaries on https://github.com/sparrowwallet/sparrow/releases.
|
||||
|
||||
Note that you will be verifying the files in the `build/jpackage/Sparrow` folder against either the `.tar.gz` or `.zip` releases.
|
||||
Download either of these depending on your platform and extract the contents to a folder (in the following example, `/tmp`).
|
||||
@@ -74,3 +131,4 @@ diff -r build/jpackage/Sparrow /tmp/Sparrow
|
||||
|
||||
This command should have no output indicating that the two folders (and all their contents) are identical.
|
||||
|
||||
If there is output, please open an issue with detailed instructions to reproduce, including build system platform.
|
||||
|
||||
+1
-1
Submodule drongo updated: 8cdea77562...fe19c86544
Vendored
BIN
Binary file not shown.
+2
-1
@@ -1,5 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -80,10 +80,10 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
@@ -143,12 +143,16 @@ fi
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
|
||||
Vendored
+1
@@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
|
||||
@@ -5,5 +5,6 @@ Exec=/opt/sparrow/bin/Sparrow %U
|
||||
Icon=/opt/sparrow/lib/Sparrow.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Unknown
|
||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||
Categories=Finance;Network;
|
||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||
StartupWMClass=Sparrow
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.6.6</string>
|
||||
<string>1.7.4</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -12,7 +12,7 @@ public class AboutController {
|
||||
private Label title;
|
||||
|
||||
public void initializeView() {
|
||||
title.setText(MainApp.APP_NAME + " " + MainApp.APP_VERSION + MainApp.APP_VERSION_SUFFIX);
|
||||
title.setText(SparrowWallet.APP_NAME + " " + SparrowWallet.APP_VERSION + SparrowWallet.APP_VERSION_SUFFIX);
|
||||
}
|
||||
|
||||
public void setStage(Stage stage) {
|
||||
|
||||
@@ -35,7 +35,6 @@ import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import de.codecentric.centerdevice.MenuToolkit;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
@@ -132,6 +131,9 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private ToggleGroup bitcoinUnit;
|
||||
|
||||
@FXML
|
||||
private ToggleGroup unitFormat;
|
||||
|
||||
@FXML
|
||||
private ToggleGroup theme;
|
||||
|
||||
@@ -182,6 +184,9 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem showPayNym;
|
||||
|
||||
@FXML
|
||||
private Menu switchServer;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem preventSleep;
|
||||
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
|
||||
@@ -217,6 +222,41 @@ public class AppController implements Initializable {
|
||||
Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight()));
|
||||
};
|
||||
|
||||
private final ListChangeListener<Tab> tabsChangeListener = (c) -> {
|
||||
if(c.next() && (c.wasAdded() || c.wasRemoved())) {
|
||||
if(c.wasRemoved() && previouslySelectedTab != null) {
|
||||
tabs.getSelectionModel().select(previouslySelectedTab);
|
||||
}
|
||||
|
||||
boolean walletAdded = c.getAddedSubList().stream().anyMatch(tab -> ((TabData) tab.getUserData()).getType() == TabData.TabType.WALLET);
|
||||
boolean walletRemoved = c.getRemoved().stream().anyMatch(tab -> ((TabData) tab.getUserData()).getType() == TabData.TabType.WALLET);
|
||||
if(walletAdded || walletRemoved) {
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
|
||||
}
|
||||
|
||||
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().filter(tab -> tab.getUserData() instanceof WalletTabData)
|
||||
.flatMap(tab -> ((TabPane) tab.getContent()).getTabs().stream().map(subTab -> (WalletTabData) subTab.getUserData())).collect(Collectors.toList());
|
||||
if(!closedWalletTabs.isEmpty()) {
|
||||
EventManager.get().post(new WalletTabsClosedEvent(closedWalletTabs));
|
||||
}
|
||||
|
||||
List<TransactionTabData> closedTransactionTabs = c.getRemoved().stream().map(tab -> (TabData) tab.getUserData())
|
||||
.filter(tabData -> tabData.getType() == TabData.TabType.TRANSACTION).map(tabData -> (TransactionTabData) tabData).collect(Collectors.toList());
|
||||
if(!closedTransactionTabs.isEmpty()) {
|
||||
EventManager.get().post(new TransactionTabsClosedEvent(closedTransactionTabs));
|
||||
}
|
||||
|
||||
closeTab.setDisable(tabs.getTabs().isEmpty());
|
||||
if(tabs.getTabs().isEmpty()) {
|
||||
Stage tabStage = (Stage) tabs.getScene().getWindow();
|
||||
tabStage.setTitle("Sparrow");
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(true);
|
||||
exportWallet.setDisable(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
@@ -273,40 +313,7 @@ public class AppController implements Initializable {
|
||||
|
||||
//Draggle tabs introduce unwanted movement when selecting between them
|
||||
//tabs.setTabDragPolicy(TabPane.TabDragPolicy.REORDER);
|
||||
tabs.getTabs().addListener((ListChangeListener<Tab>) c -> {
|
||||
if(c.next() && (c.wasAdded() || c.wasRemoved())) {
|
||||
if(c.wasRemoved() && previouslySelectedTab != null) {
|
||||
tabs.getSelectionModel().select(previouslySelectedTab);
|
||||
}
|
||||
|
||||
boolean walletAdded = c.getAddedSubList().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
|
||||
boolean walletRemoved = c.getRemoved().stream().anyMatch(tab -> ((TabData)tab.getUserData()).getType() == TabData.TabType.WALLET);
|
||||
if(walletAdded || walletRemoved) {
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
|
||||
}
|
||||
|
||||
List<WalletTabData> closedWalletTabs = c.getRemoved().stream().filter(tab -> tab.getUserData() instanceof WalletTabData)
|
||||
.flatMap(tab -> ((TabPane)tab.getContent()).getTabs().stream().map(subTab -> (WalletTabData)subTab.getUserData())).collect(Collectors.toList());
|
||||
if(!closedWalletTabs.isEmpty()) {
|
||||
EventManager.get().post(new WalletTabsClosedEvent(closedWalletTabs));
|
||||
}
|
||||
|
||||
List<TransactionTabData> closedTransactionTabs = c.getRemoved().stream().map(tab -> (TabData)tab.getUserData())
|
||||
.filter(tabData -> tabData.getType() == TabData.TabType.TRANSACTION).map(tabData -> (TransactionTabData)tabData).collect(Collectors.toList());
|
||||
if(!closedTransactionTabs.isEmpty()) {
|
||||
EventManager.get().post(new TransactionTabsClosedEvent(closedTransactionTabs));
|
||||
}
|
||||
|
||||
closeTab.setDisable(tabs.getTabs().isEmpty());
|
||||
if(tabs.getTabs().isEmpty()) {
|
||||
Stage tabStage = (Stage)tabs.getScene().getWindow();
|
||||
tabStage.setTitle("Sparrow");
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(true);
|
||||
exportWallet.setDisable(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
tabs.getTabs().addListener(tabsChangeListener);
|
||||
|
||||
tabs.getScene().getWindow().setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
@@ -322,6 +329,15 @@ public class AppController implements Initializable {
|
||||
Optional<Toggle> selectedUnitToggle = bitcoinUnit.getToggles().stream().filter(toggle -> selectedUnit.equals(toggle.getUserData())).findFirst();
|
||||
selectedUnitToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle));
|
||||
|
||||
UnitFormat format = Config.get().getUnitFormat();
|
||||
if(format == null) {
|
||||
format = UnitFormat.DOT;
|
||||
Config.get().setUnitFormat(format);
|
||||
}
|
||||
final UnitFormat selectedFormat = format;
|
||||
Optional<Toggle> selectedFormatToggle = unitFormat.getToggles().stream().filter(toggle -> selectedFormat.equals(toggle.getUserData())).findFirst();
|
||||
selectedFormatToggle.ifPresent(toggle -> unitFormat.selectToggle(toggle));
|
||||
|
||||
Theme configTheme = Config.get().getTheme();
|
||||
if(configTheme == null) {
|
||||
configTheme = Theme.LIGHT;
|
||||
@@ -367,9 +383,10 @@ public class AppController implements Initializable {
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||
});
|
||||
|
||||
configureSwitchServer();
|
||||
setServerType(Config.get().getServerType());
|
||||
serverToggle.setSelected(isConnected());
|
||||
serverToggle.setDisable(Config.get().getServerType() == null);
|
||||
serverToggle.setDisable(!Config.get().hasServer());
|
||||
onlineProperty().bindBidirectional(serverToggle.selectedProperty());
|
||||
onlineProperty().addListener(new WeakChangeListener<>(serverToggleOnlineListener));
|
||||
serverToggle.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
@@ -377,6 +394,7 @@ public class AppController implements Initializable {
|
||||
});
|
||||
|
||||
openTransactionIdItem.disableProperty().bind(onlineProperty().not());
|
||||
setNetworkLabel();
|
||||
}
|
||||
|
||||
private void setPlatformApplicationMenu() {
|
||||
@@ -386,10 +404,10 @@ public class AppController implements Initializable {
|
||||
MenuItem preferences = new MenuItem("Preferences...");
|
||||
preferences.setOnAction(this::openPreferences);
|
||||
preferences.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN));
|
||||
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(MainApp.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
|
||||
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(SparrowWallet.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
|
||||
preferences, new SeparatorMenuItem(),
|
||||
tk.createHideMenuItem(MainApp.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
|
||||
tk.createQuitMenuItem(MainApp.APP_NAME));
|
||||
tk.createHideMenuItem(SparrowWallet.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
|
||||
tk.createQuitMenuItem(SparrowWallet.APP_NAME));
|
||||
tk.setApplicationMenu(defaultApplicationMenu);
|
||||
|
||||
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||
@@ -404,12 +422,27 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
private void setNetworkLabel() {
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
Platform.runLater(() -> {
|
||||
StackPane tabBackground = (StackPane)tabs.lookup(".tab-header-background");
|
||||
if(tabBackground != null) {
|
||||
HBox hBox = new HBox();
|
||||
Label label = new Label(Network.get().toDisplayString());
|
||||
label.setPadding(new Insets(0, 10, 0, 0));
|
||||
hBox.getChildren().add(label);
|
||||
hBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
tabBackground.getChildren().add(hBox);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void showIntroduction(ActionEvent event) {
|
||||
WelcomeDialog welcomeDialog = new WelcomeDialog();
|
||||
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
|
||||
if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER);
|
||||
preferencesDialog.showAndWait();
|
||||
openPreferences(PreferenceGroup.SERVER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,7 +483,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
Stage stage = new Stage(StageStyle.UNDECORATED);
|
||||
stage.setTitle("About " + MainApp.APP_NAME);
|
||||
stage.setTitle("About " + SparrowWallet.APP_NAME);
|
||||
stage.initOwner(tabs.getScene().getWindow());
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.setResizable(false);
|
||||
@@ -720,7 +753,7 @@ public class AppController implements Initializable {
|
||||
writer.print(transactionTabData.getPsbt().toBase64String(includeXpubs));
|
||||
writer.flush();
|
||||
} else {
|
||||
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs));
|
||||
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true));
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving PSBT", e);
|
||||
@@ -855,6 +888,13 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new BitcoinUnitChangedEvent(unit));
|
||||
}
|
||||
|
||||
public void setUnitFormat(ActionEvent event) {
|
||||
MenuItem item = (MenuItem)event.getSource();
|
||||
UnitFormat format = (UnitFormat)item.getUserData();
|
||||
Config.get().setUnitFormat(format);
|
||||
EventManager.get().post(new UnitFormatChangedEvent(format));
|
||||
}
|
||||
|
||||
public void preventSleep(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setPreventSleep(item.isSelected());
|
||||
@@ -902,8 +942,10 @@ public class AppController implements Initializable {
|
||||
|
||||
private String getServerToggleTooltipText(Integer currentBlockHeight) {
|
||||
if(AppServices.isConnected()) {
|
||||
return "Connected to " + Config.get().getServerAddress() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") +
|
||||
return "Connected to " + Config.get().getServerDisplayName() + (currentBlockHeight != null ? " at height " + currentBlockHeight : "") +
|
||||
(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER ? "\nWarning! You are connected to a public server and sharing your transaction data with it.\nFor better privacy, consider using your own Bitcoin Core node or private Electrum server." : "");
|
||||
} else if(AppServices.isConnecting()) {
|
||||
return "Connecting...";
|
||||
}
|
||||
|
||||
return "Disconnected";
|
||||
@@ -996,7 +1038,7 @@ public class AppController implements Initializable {
|
||||
} catch(Exception e) {
|
||||
if(e instanceof IOException && e.getMessage().startsWith("The process cannot access the file because another process has locked")) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", "The wallet file is locked. Is another instance of " + MainApp.APP_NAME + " already running?");
|
||||
showErrorDialog("Error Opening Wallet", "The wallet file is locked. Is another instance of " + SparrowWallet.APP_NAME + " already running?");
|
||||
} else if(!attemptImportWallet(file, null)) {
|
||||
log.error("Error opening wallet", e);
|
||||
showErrorDialog("Error Opening Wallet", e.getMessage() == null ? "Unsupported file format" : e.getMessage());
|
||||
@@ -1006,8 +1048,7 @@ public class AppController implements Initializable {
|
||||
|
||||
private void openWallet(Storage storage, WalletAndKey walletAndKey, AppController appController, boolean forceSameWindow) {
|
||||
try {
|
||||
checkWalletNetwork(walletAndKey.getWallet());
|
||||
restorePublicKeysFromSeed(storage, walletAndKey.getWallet(), walletAndKey.getKey());
|
||||
storage.restorePublicKeysFromSeed(walletAndKey.getWallet(), walletAndKey.getKey());
|
||||
if(!walletAndKey.getWallet().isValid()) {
|
||||
throw new IllegalStateException("Wallet file is not valid.");
|
||||
}
|
||||
@@ -1026,103 +1067,26 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
private void checkWalletNetwork(Wallet wallet) {
|
||||
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.get()) {
|
||||
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.get() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
|
||||
}
|
||||
}
|
||||
|
||||
private void restorePublicKeysFromSeed(Storage storage, Wallet wallet, Key key) throws MnemonicException {
|
||||
if(wallet.containsMasterPrivateKeys()) {
|
||||
//Derive xpub and master fingerprint from seed, potentially with passphrase
|
||||
Wallet copy = wallet.copy();
|
||||
for(int i = 0; i < copy.getKeystores().size(); i++) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
if(copyKeystore.hasSeed() && copyKeystore.getSeed().getPassphrase() == null) {
|
||||
if(copyKeystore.getSeed().needsPassphrase()) {
|
||||
if(!wallet.isMasterWallet() && wallet.getMasterWallet().getKeystores().size() == copy.getKeystores().size() && wallet.getMasterWallet().getKeystores().get(i).hasSeed()) {
|
||||
copyKeystore.getSeed().setPassphrase(wallet.getMasterWallet().getKeystores().get(i).getSeed().getPassphrase());
|
||||
} else {
|
||||
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getFullDisplayName(), copyKeystore);
|
||||
Optional<String> optionalPassphrase = passphraseDialog.showAndWait();
|
||||
if(optionalPassphrase.isPresent()) {
|
||||
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
copyKeystore.getSeed().setPassphrase("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(wallet.isEncrypted()) {
|
||||
if(key == null) {
|
||||
throw new IllegalStateException("Wallet was not encrypted, but seed is");
|
||||
}
|
||||
|
||||
copy.decrypt(key);
|
||||
}
|
||||
|
||||
if(wallet.isWhirlpoolMasterWallet()) {
|
||||
String walletId = storage.getWalletId(wallet);
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
|
||||
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
soroban.setHDWallet(copy);
|
||||
}
|
||||
|
||||
StandardAccount standardAccount = wallet.getStandardAccountType();
|
||||
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
|
||||
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
|
||||
}
|
||||
|
||||
for(int i = 0; i < wallet.getKeystores().size(); i++) {
|
||||
Keystore keystore = wallet.getKeystores().get(i);
|
||||
if(keystore.hasSeed()) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
Keystore derivedKeystore = Keystore.fromSeed(copyKeystore.getSeed(), copyKeystore.getKeyDerivation().getDerivation());
|
||||
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
||||
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
||||
keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase());
|
||||
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
||||
copyKeystore.getSeed().clear();
|
||||
} else if(keystore.hasMasterPrivateExtendedKey()) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
Keystore derivedKeystore = Keystore.fromMasterPrivateExtendedKey(copyKeystore.getMasterPrivateExtendedKey(), copyKeystore.getKeyDerivation().getDerivation());
|
||||
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
||||
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
||||
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
||||
copyKeystore.getMasterPrivateKey().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isBip47()) {
|
||||
try {
|
||||
Keystore masterKeystore = wallet.getKeystores().get(0);
|
||||
Keystore keystore = childWallet.getKeystores().get(0);
|
||||
keystore.setBip47ExtendedPrivateKey(masterKeystore.getBip47ExtendedPrivateKey());
|
||||
List<ChildNumber> derivation = keystore.getKeyDerivation().getDerivation();
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterKeystore.getKeyDerivation().getMasterFingerprint(), derivation));
|
||||
DeterministicKey pubKey = keystore.getBip47ExtendedPrivateKey().getKey().dropPrivateBytes().dropParent();
|
||||
keystore.setExtendedPublicKey(new ExtendedKey(pubKey, keystore.getBip47ExtendedPrivateKey().getParentFingerprint(), derivation.get(derivation.size() - 1)));
|
||||
} catch(Exception e) {
|
||||
log.error("Cannot prepare BIP47 keystore", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void importWallet(ActionEvent event) {
|
||||
WalletImportDialog dlg = new WalletImportDialog();
|
||||
List<WalletForm> selectedWalletForms = getSelectedWalletForms();
|
||||
WalletImportDialog dlg = new WalletImportDialog(selectedWalletForms);
|
||||
Optional<Wallet> optionalWallet = dlg.showAndWait();
|
||||
if(optionalWallet.isPresent()) {
|
||||
Wallet wallet = optionalWallet.get();
|
||||
addImportedWallet(wallet);
|
||||
|
||||
List<WalletTabData> walletTabData = getOpenWalletTabData();
|
||||
List<ExtendedKey> xpubs = wallet.getKeystores().stream().map(Keystore::getExtendedPublicKey).collect(Collectors.toList());
|
||||
Optional<WalletForm> optNewWalletForm = walletTabData.stream()
|
||||
.map(WalletTabData::getWalletForm)
|
||||
.filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI &&
|
||||
wf.getSettingsWalletForm().getWallet().getScriptType() == wallet.getScriptType() && !wf.getSettingsWalletForm().getWallet().isValid() &&
|
||||
wf.getSettingsWalletForm().getWallet().getKeystores().stream().map(Keystore::getExtendedPublicKey).anyMatch(xpubs::contains)).findFirst();
|
||||
if(optNewWalletForm.isPresent()) {
|
||||
EventManager.get().post(new ExistingWalletImportedEvent(optNewWalletForm.get().getWalletId(), wallet));
|
||||
selectTab(optNewWalletForm.get().getWallet());
|
||||
} else if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) {
|
||||
addImportedWallet(wallet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1136,6 +1100,10 @@ public class AppController implements Initializable {
|
||||
new KeystoneSinglesig(), new KeystoneMultisig(),
|
||||
new CaravanMultisig());
|
||||
for(WalletImport importer : walletImporters) {
|
||||
if(importer.isDeprecated() && !Config.get().isShowDeprecatedImportExport()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try(FileInputStream inputStream = new FileInputStream(file)) {
|
||||
if(importer.isEncrypted(file) && password == null) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
@@ -1160,7 +1128,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void addImportedWallet(Wallet wallet) {
|
||||
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName());
|
||||
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
|
||||
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
|
||||
if(optNameAndBirthDate.isPresent()) {
|
||||
WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get();
|
||||
@@ -1212,14 +1180,12 @@ public class AppController implements Initializable {
|
||||
try {
|
||||
storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY);
|
||||
storage.saveWallet(wallet);
|
||||
checkWalletNetwork(wallet);
|
||||
restorePublicKeysFromSeed(storage, wallet, null);
|
||||
storage.restorePublicKeysFromSeed(wallet, null);
|
||||
addWalletTabOrWindow(storage, wallet, false);
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
storage.saveWallet(childWallet);
|
||||
checkWalletNetwork(childWallet);
|
||||
restorePublicKeysFromSeed(storage, childWallet, null);
|
||||
storage.restorePublicKeysFromSeed(childWallet, null);
|
||||
addWalletTabOrWindow(storage, childWallet, false);
|
||||
}
|
||||
Platform.runLater(() -> selectTab(wallet));
|
||||
@@ -1239,8 +1205,7 @@ public class AppController implements Initializable {
|
||||
wallet.encrypt(key);
|
||||
storage.setEncryptionPubKey(encryptionPubKey);
|
||||
storage.saveWallet(wallet);
|
||||
checkWalletNetwork(wallet);
|
||||
restorePublicKeysFromSeed(storage, wallet, key);
|
||||
storage.restorePublicKeysFromSeed(wallet, key);
|
||||
addWalletTabOrWindow(storage, wallet, false);
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
@@ -1248,8 +1213,7 @@ public class AppController implements Initializable {
|
||||
childWallet.encrypt(key);
|
||||
}
|
||||
storage.saveWallet(childWallet);
|
||||
checkWalletNetwork(childWallet);
|
||||
restorePublicKeysFromSeed(storage, childWallet, key);
|
||||
storage.restorePublicKeysFromSeed(childWallet, key);
|
||||
addWalletTabOrWindow(storage, childWallet, false);
|
||||
}
|
||||
Platform.runLater(() -> selectTab(wallet));
|
||||
@@ -1284,13 +1248,18 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
public void openPreferences(ActionEvent event) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog();
|
||||
preferencesDialog.showAndWait();
|
||||
openPreferences(PreferenceGroup.GENERAL);
|
||||
}
|
||||
|
||||
public void openServerPreferences(ActionEvent event) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER);
|
||||
openPreferences(PreferenceGroup.SERVER);
|
||||
}
|
||||
|
||||
private void openPreferences(PreferenceGroup preferenceGroup) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(preferenceGroup);
|
||||
preferencesDialog.showAndWait();
|
||||
configureSwitchServer();
|
||||
serverToggle.setDisable(!Config.get().hasServer());
|
||||
}
|
||||
|
||||
public void signVerifyMessage(ActionEvent event) {
|
||||
@@ -1511,11 +1480,10 @@ public class AppController implements Initializable {
|
||||
wallet.setName(name);
|
||||
}
|
||||
Tab tab = new Tab("");
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
|
||||
glyph.setFontSize(10.0);
|
||||
glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
|
||||
WalletIcon walletIcon = new WalletIcon(storage, wallet);
|
||||
walletIcon.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
|
||||
Label tabLabel = new Label(name);
|
||||
tabLabel.setGraphic(glyph);
|
||||
tabLabel.setGraphic(walletIcon);
|
||||
tabLabel.setGraphicTextGap(5.0);
|
||||
tab.setGraphic(tabLabel);
|
||||
tab.setClosable(true);
|
||||
@@ -1647,7 +1615,7 @@ public class AppController implements Initializable {
|
||||
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
|
||||
subTab.setUserData(tabData);
|
||||
if(!wallet.isWhirlpoolChildWallet()) {
|
||||
subTab.setContextMenu(getSubTabContextMenu(wallet, subTabs, subTab));
|
||||
subTab.setContextMenu(getSubTabContextMenu(storage, wallet, subTabs, subTab));
|
||||
}
|
||||
|
||||
subTabs.getTabs().add(subTab);
|
||||
@@ -1658,6 +1626,10 @@ public class AppController implements Initializable {
|
||||
});
|
||||
subTabs.getSelectionModel().select(subTab);
|
||||
|
||||
if(wallet.isValid()) {
|
||||
Platform.runLater(() -> walletForm.refreshHistory(AppServices.getCurrentBlockHeight()));
|
||||
}
|
||||
|
||||
return walletForm;
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
@@ -1696,6 +1668,19 @@ public class AppController implements Initializable {
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<WalletForm> getSelectedWalletForms() {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
if(selectedTab != null) {
|
||||
TabData tabData = (TabData) selectedTab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane) selectedTab.getContent();
|
||||
return subTabs.getTabs().stream().map(subTab -> ((WalletTabData) subTab.getUserData()).getWalletForm()).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private void addTransactionTab(String name, File file, String string) throws ParseException, PSBTParseException, TransactionParseException {
|
||||
if(Utils.isBase64(string) && !Utils.isHex(string)) {
|
||||
addTransactionTab(name, file, Base64.getDecoder().decode(string));
|
||||
@@ -1860,6 +1845,31 @@ public class AppController implements Initializable {
|
||||
contextMenu.getItems().addAll(lock);
|
||||
}
|
||||
|
||||
MenuItem moveRight = new MenuItem("Move Right");
|
||||
moveRight.setOnAction(event -> {
|
||||
int index = tabs.getTabs().indexOf(tab);
|
||||
tabs.getTabs().removeListener(tabsChangeListener);
|
||||
tabs.getTabs().remove(tab);
|
||||
tabs.getTabs().add(index + 1, tab);
|
||||
tabs.getTabs().addListener(tabsChangeListener);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
});
|
||||
MenuItem moveLeft = new MenuItem("Move Left");
|
||||
moveLeft.setOnAction(event -> {
|
||||
int index = tabs.getTabs().indexOf(tab);
|
||||
tabs.getTabs().removeListener(tabsChangeListener);
|
||||
tabs.getTabs().remove(tab);
|
||||
tabs.getTabs().add(index - 1, tab);
|
||||
tabs.getTabs().addListener(tabsChangeListener);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
});
|
||||
contextMenu.getItems().addAll(moveRight, moveLeft);
|
||||
|
||||
contextMenu.setOnShowing(event -> {
|
||||
moveRight.setDisable(tabs.getTabs().indexOf(tab) == tabs.getTabs().size() - 1);
|
||||
moveLeft.setDisable(tabs.getTabs().indexOf(tab) == 0);
|
||||
});
|
||||
|
||||
MenuItem close = new MenuItem("Close");
|
||||
close.setOnAction(event -> {
|
||||
tabs.getTabs().remove(tab);
|
||||
@@ -1877,15 +1887,63 @@ public class AppController implements Initializable {
|
||||
tabs.getTabs().removeAll(tabs.getTabs());
|
||||
});
|
||||
|
||||
MenuItem delete = new MenuItem("Delete...");
|
||||
delete.setOnAction(event -> {
|
||||
deleteWallet(getSelectedWalletForm());
|
||||
});
|
||||
contextMenu.getItems().addAll(new SeparatorMenuItem(), close, closeOthers, closeAll);
|
||||
|
||||
if(tab.getUserData() instanceof WalletTabData walletTabData) {
|
||||
Menu walletIcon = new Menu("Wallet Icon");
|
||||
MenuItem custom = new MenuItem("Custom...");
|
||||
custom.setOnAction(event -> {
|
||||
setCustomIcon(walletTabData.getWallet());
|
||||
});
|
||||
MenuItem reset = new MenuItem("Reset");
|
||||
reset.setOnAction(event -> {
|
||||
resetIcon(walletTabData.getWalletForm());
|
||||
});
|
||||
walletIcon.getItems().addAll(custom, reset);
|
||||
|
||||
MenuItem delete = new MenuItem("Delete...");
|
||||
delete.setOnAction(event -> {
|
||||
deleteWallet(walletTabData.getWalletForm());
|
||||
});
|
||||
contextMenu.getItems().addAll(new SeparatorMenuItem(), walletIcon, delete);
|
||||
}
|
||||
|
||||
contextMenu.getItems().addAll(close, closeOthers, closeAll, delete);
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
private void setCustomIcon(Wallet wallet) {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Image");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("Images", "*.png", "*.jpg", "*.jpeg", "*.gif")
|
||||
);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
if(file != null) {
|
||||
try {
|
||||
byte[] iconData = ImageUtils.resize(file, WalletIcon.SAVE_WIDTH, WalletIcon.SAVE_HEIGHT);
|
||||
WalletConfig walletConfig = wallet.getMasterWalletConfig();
|
||||
walletConfig.setIconData(iconData, true);
|
||||
EventManager.get().post(new WalletConfigChangedEvent(wallet));
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating custom wallet icon", e);
|
||||
showErrorDialog("Error creating custom wallet icon", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void resetIcon(WalletForm walletForm) {
|
||||
Wallet masterWallet = walletForm.getMasterWallet();
|
||||
if(masterWallet.getWalletConfig() != null && masterWallet.getWalletConfig().isUserIcon()) {
|
||||
masterWallet.getWalletConfig().setIconData(null, false);
|
||||
EventManager.get().post(new WalletConfigChangedEvent(masterWallet));
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteWallet(WalletForm selectedWalletForm) {
|
||||
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Delete " + selectedWalletForm.getWallet().getMasterName() + "?", "The wallet file and any backups will be deleted. Are you sure?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
@@ -1930,13 +1988,30 @@ public class AppController implements Initializable {
|
||||
|
||||
private void deleteStorage(Storage storage) {
|
||||
if(storage.isClosed()) {
|
||||
Platform.runLater(storage::delete);
|
||||
Platform.runLater(() -> {
|
||||
Storage.DeleteWalletService deleteWalletService = new Storage.DeleteWalletService(storage);
|
||||
deleteWalletService.setDelay(Duration.seconds(3));
|
||||
deleteWalletService.setPeriod(Duration.hours(1));
|
||||
deleteWalletService.setOnSucceeded(event -> {
|
||||
deleteWalletService.cancel();
|
||||
if(!deleteWalletService.getValue()) {
|
||||
showErrorDialog("Error deleting wallet", "Could not delete " + storage.getWalletFile().getName() + ". Please delete this file manually.");
|
||||
}
|
||||
});
|
||||
deleteWalletService.setOnFailed(event -> {
|
||||
deleteWalletService.cancel();
|
||||
showErrorDialog("Error deleting wallet", "Could not delete " + storage.getWalletFile().getName() + ". Please delete this file manually.");
|
||||
});
|
||||
deleteWalletService.start();
|
||||
});
|
||||
} else {
|
||||
Platform.runLater(() -> deleteStorage(storage));
|
||||
}
|
||||
}
|
||||
|
||||
private ContextMenu getSubTabContextMenu(Wallet wallet, TabPane subTabs, Tab subTab) {
|
||||
private ContextMenu getSubTabContextMenu(Storage storage, Wallet wallet, TabPane subTabs, Tab subTab) {
|
||||
String walletId = storage.getWalletId(wallet);
|
||||
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
MenuItem rename = new MenuItem("Rename Account");
|
||||
rename.setOnAction(event -> {
|
||||
@@ -1947,8 +2022,9 @@ public class AppController implements Initializable {
|
||||
String label = optLabel.get();
|
||||
subTabLabel.setText(label);
|
||||
|
||||
wallet.setLabel(label);
|
||||
EventManager.get().post(new WalletLabelChangedEvent(wallet));
|
||||
Wallet renamedWallet = AppServices.get().getWallet(walletId);
|
||||
renamedWallet.setLabel(label);
|
||||
EventManager.get().post(new WalletLabelChangedEvent(renamedWallet));
|
||||
}
|
||||
});
|
||||
contextMenu.getItems().add(rename);
|
||||
@@ -1958,11 +2034,12 @@ public class AppController implements Initializable {
|
||||
delete.setOnAction(event -> {
|
||||
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Delete Wallet Account?", "Labels applied in this wallet account will be lost. Are you sure?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.OK) {
|
||||
Wallet deletedWallet = AppServices.get().getWallet(walletId);
|
||||
EventManager.get().post(new WalletDeletedEvent(deletedWallet));
|
||||
subTabs.getTabs().remove(subTab);
|
||||
if(subTabs.getTabs().size() == 1) {
|
||||
setSubTabsVisible(subTabs, areSubTabsVisible());
|
||||
}
|
||||
EventManager.get().post(new WalletDeletedEvent(wallet));
|
||||
}
|
||||
});
|
||||
contextMenu.getItems().add(delete);
|
||||
@@ -1971,6 +2048,48 @@ public class AppController implements Initializable {
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
private void configureSwitchServer() {
|
||||
switchServer.getItems().clear();
|
||||
|
||||
Config config = Config.get();
|
||||
if(config.getServerType() == ServerType.BITCOIN_CORE && config.getRecentCoreServers() != null && config.getRecentCoreServers().size() > 1) {
|
||||
for(Server server : config.getRecentCoreServers()) {
|
||||
switchServer.getItems().add(getSwitchServerMenuItem(ServerType.BITCOIN_CORE, server));
|
||||
}
|
||||
} else if(config.getServerType() == ServerType.ELECTRUM_SERVER && config.getRecentElectrumServers() != null && config.getRecentElectrumServers().size() > 1) {
|
||||
for(Server server : config.getRecentElectrumServers()) {
|
||||
switchServer.getItems().add(getSwitchServerMenuItem(ServerType.ELECTRUM_SERVER, server));
|
||||
}
|
||||
}
|
||||
|
||||
switchServer.setVisible(!switchServer.getItems().isEmpty());
|
||||
}
|
||||
|
||||
private CheckMenuItem getSwitchServerMenuItem(ServerType serverType, Server server) {
|
||||
CheckMenuItem checkMenuItem = new CheckMenuItem(server.getDisplayName());
|
||||
boolean selected = (serverType == ServerType.BITCOIN_CORE ? server.equals(Config.get().getCoreServer()) : server.equals(Config.get().getElectrumServer()));
|
||||
checkMenuItem.setSelected(selected);
|
||||
checkMenuItem.setOnAction(event -> {
|
||||
if(!selected) {
|
||||
boolean online = onlineProperty().get();
|
||||
onlineProperty().set(false);
|
||||
if(serverType == ServerType.BITCOIN_CORE) {
|
||||
Config.get().setCoreServer(server);
|
||||
} else if(serverType == ServerType.ELECTRUM_SERVER) {
|
||||
Config.get().setElectrumServer(server);
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
onlineProperty().set(online);
|
||||
configureSwitchServer();
|
||||
});
|
||||
} else {
|
||||
checkMenuItem.setSelected(true);
|
||||
}
|
||||
});
|
||||
|
||||
return checkMenuItem;
|
||||
}
|
||||
|
||||
public void setServerType(ServerType serverType) {
|
||||
if(serverType == ServerType.PUBLIC_ELECTRUM_SERVER && !serverToggle.getStyleClass().contains("public-server")) {
|
||||
serverToggle.getStyleClass().add("public-server");
|
||||
@@ -1984,7 +2103,7 @@ public class AppController implements Initializable {
|
||||
serverToggle.getStyleClass().remove("core-server");
|
||||
}
|
||||
|
||||
serverToggle.setDisable(false);
|
||||
serverToggle.setDisable(!Config.get().hasServer());
|
||||
}
|
||||
|
||||
public void setTheme(ActionEvent event) {
|
||||
@@ -2033,8 +2152,8 @@ public class AppController implements Initializable {
|
||||
|
||||
private void tabLabelAddFailure(Tab tab) {
|
||||
Label tabLabel = (Label)tab.getGraphic();
|
||||
if(!tabLabel.getStyleClass().contains("failure")) {
|
||||
tabLabel.getGraphic().getStyleClass().add("failure");
|
||||
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
|
||||
if(walletIcon.addFailure()) {
|
||||
tabLabel.setTooltip(new Tooltip("Error loading transaction history from server"));
|
||||
}
|
||||
}
|
||||
@@ -2067,7 +2186,8 @@ public class AppController implements Initializable {
|
||||
|
||||
private void tabLabelRemoveFailure(Tab tab) {
|
||||
Label tabLabel = (Label)tab.getGraphic();
|
||||
tabLabel.getGraphic().getStyleClass().remove("failure");
|
||||
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
|
||||
walletIcon.removeFailure();
|
||||
tabLabel.setTooltip(null);
|
||||
}
|
||||
|
||||
@@ -2104,6 +2224,14 @@ public class AppController implements Initializable {
|
||||
} else {
|
||||
tabs.getScene().getStylesheets().remove(darkCss);
|
||||
}
|
||||
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
Label tabLabel = (Label)tab.getGraphic();
|
||||
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
|
||||
walletIcon.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -2184,6 +2312,9 @@ public class AppController implements Initializable {
|
||||
Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0));
|
||||
Label masterLabel = (Label)masterTab.getGraphic();
|
||||
masterLabel.setText(event.getWallet().getLabel() != null ? event.getWallet().getLabel() : event.getWallet().getAutomaticName());
|
||||
Label tabLabel = (Label)walletTab.getGraphic();
|
||||
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
|
||||
walletIcon.setWallet(event.getWallet());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2399,11 +2530,12 @@ public class AppController implements Initializable {
|
||||
|
||||
@Subscribe
|
||||
public void connection(ConnectionEvent event) {
|
||||
String status = "Connected to " + Config.get().getServerAddress() + " at height " + event.getBlockHeight();
|
||||
String status = "Connected to " + Config.get().getServerDisplayName() + " at height " + event.getBlockHeight();
|
||||
statusUpdated(new StatusEvent(status));
|
||||
setServerToggleTooltip(event.getBlockHeight());
|
||||
serverToggleStopAnimation();
|
||||
setTorIcon();
|
||||
configureSwitchServer();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -2493,6 +2625,50 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void cormorantSyncStatus(CormorantSyncStatusEvent event) {
|
||||
serverToggle.setDisable(false);
|
||||
if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) {
|
||||
statusUpdated(new StatusEvent("Syncing... (" + event.getProgress() + "% complete, synced to " + event.getTipAsString() + ")"));
|
||||
if(event.getProgress() > 0 && (statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING)) {
|
||||
statusBar.setProgress((double)event.getProgress() / 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void cormorantScanStatus(CormorantScanStatusEvent event) {
|
||||
serverToggle.setDisable(true);
|
||||
if((AppServices.isConnecting() || AppServices.isConnected()) && !event.isCompleted()) {
|
||||
statusUpdated(new StatusEvent("Scanning... (" + event.getProgress() + "% complete" + (event.getRemainingAsString().isEmpty() ? ")" : ", " + event.getRemainingAsString() + " remaining)")));
|
||||
if(event.getProgress() > 0 && (statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING)) {
|
||||
statusBar.setProgress((double)event.getProgress() / 100);
|
||||
}
|
||||
} else if(event.isCompleted()) {
|
||||
serverToggle.setDisable(false);
|
||||
if(statusBar.getText().startsWith("Scanning...")) {
|
||||
statusBar.setText("");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void cormorantPruneStatus(CormorantPruneStatusEvent event) {
|
||||
if(event.legacyWalletExists()) {
|
||||
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("Error importing Bitcoin Core descriptor wallet",
|
||||
"The connected node is pruned at " + event.getPruneDateAsString() + ", but the wallet birthday for " + event.getWallet().getFullDisplayName() + " is set to " + event.getScanDateAsString() + ".\n\n" +
|
||||
"Do you want to try using the existing legacy Bitcoin Core wallet?", ButtonType.YES, ButtonType.NO);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
Config.get().setUseLegacyCoreWallet(true);
|
||||
onlineProperty().set(false);
|
||||
Platform.runLater(() -> onlineProperty().set(true));
|
||||
}
|
||||
} else {
|
||||
AppServices.showErrorDialog("Error importing Bitcoin Core descriptor wallet",
|
||||
"The connected node is pruned at " + event.getPruneDateAsString() + ", but the wallet birthday for " + event.getWallet().getFullDisplayName() + " is set to " + event.getScanDateAsString() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void bwtBootStatus(BwtBootStatusEvent event) {
|
||||
serverToggle.setDisable(true);
|
||||
@@ -2623,6 +2799,12 @@ public class AppController implements Initializable {
|
||||
selectedToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void unitFormatChanged(UnitFormatChangedEvent event) {
|
||||
Optional<Toggle> selectedToggle = unitFormat.getToggles().stream().filter(toggle -> event.getUnitFormat().equals(toggle.getUserData())).findFirst();
|
||||
selectedToggle.ifPresent(toggle -> unitFormat.selectToggle(toggle));
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void openWalletsInNewWindowsStatusChanged(OpenWalletsNewWindowsStatusEvent event) {
|
||||
openWalletsInNewWindows.setSelected(event.isOpenWalletsInNewWindows());
|
||||
@@ -2704,4 +2886,17 @@ public class AppController implements Initializable {
|
||||
lockAllWallets.setDisable(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletConfigChanged(WalletConfigChangedEvent event) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
if(tab.getUserData() instanceof WalletTabData walletTabData) {
|
||||
if(walletTabData.getWallet() == event.getWallet()) {
|
||||
Label tabLabel = (Label)tab.getGraphic();
|
||||
WalletIcon walletIcon = (WalletIcon)tabLabel.getGraphic();
|
||||
walletIcon.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
@@ -28,6 +27,7 @@ import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymService;
|
||||
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;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -42,7 +42,6 @@ import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
@@ -51,8 +50,6 @@ import javafx.stage.Screen;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
import org.berndpruenster.netlayer.tor.Tor;
|
||||
import org.controlsfx.control.HyperlinkLabel;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -69,8 +66,6 @@ import java.time.ZoneId;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class AppServices {
|
||||
@@ -97,9 +92,11 @@ public class AppServices {
|
||||
|
||||
private final SorobanServices sorobanServices = new SorobanServices();
|
||||
|
||||
private InteractionServices interactionServices;
|
||||
|
||||
private static PayNymService payNymService;
|
||||
|
||||
private final MainApp application;
|
||||
private final Application application;
|
||||
|
||||
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
|
||||
|
||||
@@ -173,8 +170,9 @@ public class AppServices {
|
||||
openFiles(event.getFiles(), null);
|
||||
};
|
||||
|
||||
public AppServices(MainApp application) {
|
||||
private AppServices(Application application, InteractionServices interactionServices) {
|
||||
this.application = application;
|
||||
this.interactionServices = interactionServices;
|
||||
EventManager.get().register(this);
|
||||
EventManager.get().register(whirlpoolServices);
|
||||
EventManager.get().register(sorobanServices);
|
||||
@@ -203,7 +201,7 @@ public class AppServices {
|
||||
|
||||
private void restartServices() {
|
||||
Config config = Config.get();
|
||||
if(config.hasServerAddress()) {
|
||||
if(config.hasServer()) {
|
||||
restartService(connectionService);
|
||||
}
|
||||
|
||||
@@ -211,7 +209,7 @@ public class AppServices {
|
||||
restartService(ratesService);
|
||||
}
|
||||
|
||||
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET) {
|
||||
if(config.isCheckNewVersions() && Network.get() == Network.MAINNET && Interface.get() == Interface.DESKTOP) {
|
||||
restartService(versionCheckService);
|
||||
}
|
||||
|
||||
@@ -253,7 +251,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
if(Tor.getDefault() != null) {
|
||||
Tor.getDefault().shutdown();
|
||||
Tor.getDefault().getTorManager().destroy(true, success -> {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,7 +266,7 @@ public class AppServices {
|
||||
connectionService.setOnRunning(workerStateEvent -> {
|
||||
connectionService.setDelay(Duration.ZERO);
|
||||
if(!ElectrumServer.isConnected()) {
|
||||
EventManager.get().post(new ConnectionStartEvent(Config.get().getServerAddress()));
|
||||
EventManager.get().post(new ConnectionStartEvent(Config.get().getServerDisplayName()));
|
||||
}
|
||||
});
|
||||
connectionService.setOnSucceeded(successEvent -> {
|
||||
@@ -285,26 +283,25 @@ public class AppServices {
|
||||
});
|
||||
connectionService.setOnFailed(failEvent -> {
|
||||
//Close connection here to create a new transport next time we try
|
||||
connectionService.resetConnection();
|
||||
connectionService.closeConnection();
|
||||
|
||||
if(failEvent.getSource().getException() instanceof ServerConfigException) {
|
||||
connectionService.setRestartOnFailure(false);
|
||||
}
|
||||
|
||||
if(failEvent.getSource().getException() instanceof TlsServerException && failEvent.getSource().getException().getCause() != null) {
|
||||
TlsServerException tlsServerException = (TlsServerException)failEvent.getSource().getException();
|
||||
if(failEvent.getSource().getException() instanceof TlsServerException tlsServerException && failEvent.getSource().getException().getCause() != null) {
|
||||
connectionService.setRestartOnFailure(false);
|
||||
if(tlsServerException.getCause().getMessage().contains("PKIX path building failed")) {
|
||||
File crtFile = Config.get().getElectrumServerCert();
|
||||
if(crtFile != null && Config.get().getServerType() == ServerType.ELECTRUM_SERVER) {
|
||||
AppServices.showErrorDialog("SSL Handshake Failed", "The configured server certificate at " + crtFile.getAbsolutePath() + " did not match the certificate provided by the server at " + tlsServerException.getServer().getHost() + "." +
|
||||
"\n\nThis may indicate a man-in-the-middle attack!" +
|
||||
"\n\nThis may be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." +
|
||||
"\n\nChange the configured server certificate if you would like to proceed.");
|
||||
} else {
|
||||
crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
|
||||
if(crtFile != null) {
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
|
||||
"\n\nThis may indicate a man-in-the-middle attack!" +
|
||||
"\n\nThis may be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." +
|
||||
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
|
||||
if(crtFile.delete()) {
|
||||
@@ -316,10 +313,19 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if(tlsServerException.getCause().getCause() instanceof UnknownCertificateExpiredException expiredException) {
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " has expired. "
|
||||
+ tlsServerException.getMessage() + "." +
|
||||
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
|
||||
Storage.saveCertificate(tlsServerException.getServer().getHost(), expiredException.getCertificate());
|
||||
Platform.runLater(() -> restartService(connectionService));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(failEvent.getSource().getException() instanceof ProxyServerException && Config.get().isUseProxy() && Config.get().requiresTor()) {
|
||||
if(failEvent.getSource().getException() instanceof ProxyServerException && Config.get().isUseProxy() && Config.get().isAutoSwitchProxy() && Config.get().requiresTor()) {
|
||||
Config.get().setUseProxy(false);
|
||||
Platform.runLater(() -> restartService(torService));
|
||||
return;
|
||||
@@ -407,23 +413,6 @@ public class AppServices {
|
||||
EventManager.get().post(new TorReadyStatusEvent());
|
||||
});
|
||||
torService.setOnFailed(workerStateEvent -> {
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
if(exception instanceof TorServerAlreadyBoundException) {
|
||||
String proxyServer = Config.get().getProxyServer();
|
||||
if(proxyServer == null || proxyServer.equals("")) {
|
||||
proxyServer = "localhost:9050";
|
||||
Config.get().setProxyServer(proxyServer);
|
||||
}
|
||||
|
||||
if(proxyServer.equals("localhost:9050") || proxyServer.equals("127.0.0.1:9050")) {
|
||||
Config.get().setUseProxy(true);
|
||||
torService.cancel();
|
||||
restartServices();
|
||||
EventManager.get().post(new TorExternalStatusEvent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
EventManager.get().post(new TorFailedStatusEvent(workerStateEvent.getSource().getException()));
|
||||
});
|
||||
|
||||
@@ -483,8 +472,7 @@ public class AppServices {
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
||||
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
} else if(AppServices.isTorRunning()) {
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress("localhost", TorService.PROXY_PORT);
|
||||
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
proxy = Tor.getDefault().getProxy();
|
||||
}
|
||||
|
||||
//Setting new proxy authentication credentials will force a new Tor circuit to be created
|
||||
@@ -499,8 +487,12 @@ public class AppServices {
|
||||
return proxy;
|
||||
}
|
||||
|
||||
static void initialize(MainApp application) {
|
||||
INSTANCE = new AppServices(application);
|
||||
public static void initialize(Application application) {
|
||||
INSTANCE = new AppServices(application, new DefaultInteractionServices());
|
||||
}
|
||||
|
||||
public static void initialize(Application application, InteractionServices interactionServices) {
|
||||
INSTANCE = new AppServices(application, interactionServices);
|
||||
}
|
||||
|
||||
public static AppServices get() {
|
||||
@@ -515,6 +507,10 @@ public class AppServices {
|
||||
return get().sorobanServices;
|
||||
}
|
||||
|
||||
public static InteractionServices getInteractionServices() {
|
||||
return get().interactionServices;
|
||||
}
|
||||
|
||||
public static PayNymService getPayNymService() {
|
||||
if(payNymService == null) {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
@@ -531,7 +527,7 @@ public class AppServices {
|
||||
|
||||
public static HostAndPort getTorProxy() {
|
||||
return AppServices.isTorRunning() ?
|
||||
HostAndPort.fromParts("localhost", TorService.PROXY_PORT) :
|
||||
Tor.getDefault().getProxyHostAndPort() :
|
||||
(Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer()));
|
||||
}
|
||||
|
||||
@@ -561,7 +557,7 @@ public class AppServices {
|
||||
|
||||
private static Image getWindowIcon() {
|
||||
if(windowIcon == null) {
|
||||
windowIcon = new Image(MainApp.class.getResourceAsStream("/image/sparrow-icon.png"));
|
||||
windowIcon = new Image(SparrowWallet.class.getResourceAsStream("/image/sparrow-icon.png"));
|
||||
}
|
||||
|
||||
return windowIcon;
|
||||
@@ -571,7 +567,7 @@ public class AppServices {
|
||||
return (node.getScene() != null && node.getScene().getWindow().getHeight() < 768);
|
||||
}
|
||||
|
||||
public MainApp getApplication() {
|
||||
public Application getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
@@ -746,40 +742,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
|
||||
Alert alert = new Alert(alertType, content, buttons);
|
||||
setStageIcon(alert.getDialogPane().getScene().getWindow());
|
||||
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(title);
|
||||
if(graphic != null) {
|
||||
alert.setGraphic(graphic);
|
||||
}
|
||||
|
||||
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
|
||||
Matcher matcher = linkPattern.matcher(content);
|
||||
if(matcher.find()) {
|
||||
String link = matcher.group(1);
|
||||
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(content);
|
||||
hyperlinkLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
hyperlinkLabel.setMaxHeight(Double.MAX_VALUE);
|
||||
hyperlinkLabel.getStyleClass().add("content");
|
||||
Label label = new Label();
|
||||
hyperlinkLabel.setPrefWidth(Math.max(360, TextUtils.computeTextWidth(label.getFont(), link, 0.0D) + 50));
|
||||
hyperlinkLabel.setOnAction(event -> {
|
||||
alert.close();
|
||||
get().getApplication().getHostServices().showDocument(link);
|
||||
});
|
||||
alert.getDialogPane().setContent(hyperlinkLabel);
|
||||
}
|
||||
|
||||
String[] lines = content.split("\r\n|\r|\n");
|
||||
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
|
||||
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
|
||||
}
|
||||
|
||||
moveToActiveWindowScreen(alert);
|
||||
return alert.showAndWait();
|
||||
return getInteractionServices().showAlert(title, content, alertType, graphic, buttons);
|
||||
}
|
||||
|
||||
public static void setStageIcon(Window window) {
|
||||
@@ -829,6 +792,24 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
public static void openBlockExplorer(String txid) {
|
||||
if(Config.get().isBlockExplorerDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Server blockExplorer = Config.get().getBlockExplorer() == null ? BlockExplorer.MEMPOOL_SPACE.getServer() : Config.get().getBlockExplorer();
|
||||
String url = blockExplorer.getUrl();
|
||||
if(url.contains("{0}")) {
|
||||
url = url.replace("{0}", txid);
|
||||
} else {
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
url += "/" + Network.get().getName();
|
||||
}
|
||||
url += "/tx/" + txid;
|
||||
}
|
||||
AppServices.get().getApplication().getHostServices().showDocument(url);
|
||||
}
|
||||
|
||||
static void parseFileUriArguments(List<String> fileUriArguments) {
|
||||
for(String fileUri : fileUriArguments) {
|
||||
try {
|
||||
@@ -1130,7 +1111,7 @@ public class AppServices {
|
||||
Wallet wallet = walletTabData.getWallet();
|
||||
Storage storage = walletTabData.getStorage();
|
||||
|
||||
if(!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
if(Interface.get() == Interface.DESKTOP && (!storage.getWalletFile().exists() || wallet.containsSource(KeystoreSource.HW_USB) || CardApi.isReaderAvailable())) {
|
||||
usbWallet = true;
|
||||
|
||||
if(deviceEnumerateService == null) {
|
||||
@@ -1157,12 +1138,20 @@ public class AppServices {
|
||||
|
||||
@Subscribe
|
||||
public void requestConnect(RequestConnectEvent event) {
|
||||
onlineProperty.set(true);
|
||||
if(Config.get().hasServer()) {
|
||||
onlineProperty.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void requestDisconnect(RequestDisconnectEvent event) {
|
||||
onlineProperty.set(false);
|
||||
//Ensure services don't try to reconnect later
|
||||
Platform.runLater(() -> {
|
||||
connectionService.cancel();
|
||||
ratesService.cancel();
|
||||
versionCheckService.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1209,7 +1198,7 @@ public class AppServices {
|
||||
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
|
||||
onlineProperty.set(false);
|
||||
log.warn("Failed to fetch wallet history from " + Config.get().getServerAddress() + ", reconnecting to another server...");
|
||||
log.warn("Failed to fetch wallet history from " + Config.get().getServerDisplayName() + ", reconnecting to another server...");
|
||||
Config.get().changePublicServer();
|
||||
onlineProperty.set(true);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ public class Args {
|
||||
@Parameter(names = { "--level", "-l" }, description = "Set log level")
|
||||
public Level level;
|
||||
|
||||
@Parameter(names = { "--terminal", "-t" }, description = "Terminal mode", arity = 0)
|
||||
public boolean terminal;
|
||||
|
||||
@Parameter(names = { "--version", "-v" }, description = "Show version", arity = 0)
|
||||
public boolean version;
|
||||
|
||||
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
|
||||
public boolean help;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ public abstract class BaseController {
|
||||
scriptArea.setMouseOverTextDelay(Duration.ofMillis(150));
|
||||
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
|
||||
TwoDimensional.Position position = scriptArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
|
||||
if(position.getMajor() % 2 == 0) {
|
||||
if(position.getMajor() % 2 == 0 && scriptArea.getScript().getChunks().size() > position.getMajor() / 2) {
|
||||
ScriptChunk hoverChunk = scriptArea.getScript().getChunks().get(position.getMajor()/2);
|
||||
if(!hoverChunk.isOpCode()) {
|
||||
Point2D pos = e.getScreenPosition();
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.text.Font;
|
||||
import org.controlsfx.control.HyperlinkLabel;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.moveToActiveWindowScreen;
|
||||
import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
|
||||
|
||||
public class DefaultInteractionServices implements InteractionServices {
|
||||
@Override
|
||||
public Optional<ButtonType> showAlert(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
|
||||
Alert alert = new Alert(alertType, content, buttons);
|
||||
setStageIcon(alert.getDialogPane().getScene().getWindow());
|
||||
alert.getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
alert.setTitle(title);
|
||||
alert.setHeaderText(title);
|
||||
if(graphic != null) {
|
||||
alert.setGraphic(graphic);
|
||||
}
|
||||
|
||||
Pattern linkPattern = Pattern.compile("\\[(http.+)]");
|
||||
Matcher matcher = linkPattern.matcher(content);
|
||||
if(matcher.find()) {
|
||||
String link = matcher.group(1);
|
||||
HyperlinkLabel hyperlinkLabel = new HyperlinkLabel(content);
|
||||
hyperlinkLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
hyperlinkLabel.setMaxHeight(Double.MAX_VALUE);
|
||||
hyperlinkLabel.getStyleClass().add("content");
|
||||
Label label = new Label();
|
||||
hyperlinkLabel.setPrefWidth(Math.max(360, TextUtils.computeTextWidth(label.getFont(), link, 0.0D) + 50));
|
||||
hyperlinkLabel.setOnAction(event -> {
|
||||
alert.close();
|
||||
AppServices.get().getApplication().getHostServices().showDocument(link);
|
||||
});
|
||||
alert.getDialogPane().setContent(hyperlinkLabel);
|
||||
}
|
||||
|
||||
String[] lines = content.split("\r\n|\r|\n");
|
||||
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
|
||||
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
|
||||
}
|
||||
|
||||
moveToActiveWindowScreen(alert);
|
||||
return alert.showAndWait();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<String> requestPassphrase(String walletName, Keystore keystore) {
|
||||
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(walletName, keystore);
|
||||
return passphraseDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface InteractionServices {
|
||||
Optional<ButtonType> showAlert(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons);
|
||||
Optional<String> requestPassphrase(String walletName, Keystore keystore);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
public enum Interface {
|
||||
DESKTOP, TERMINAL, SERVER;
|
||||
|
||||
private static Interface currentInterface;
|
||||
|
||||
public static Interface get() {
|
||||
if(currentInterface == null) {
|
||||
boolean headless = java.awt.GraphicsEnvironment.isHeadless();
|
||||
boolean monocle = "Monocle".equalsIgnoreCase(System.getProperty("glass.platform"));
|
||||
|
||||
if(headless || monocle) {
|
||||
currentInterface = TERMINAL;
|
||||
|
||||
if(headless && !monocle) {
|
||||
throw new UnsupportedOperationException("Headless environment detected but Monocle platform not found");
|
||||
}
|
||||
} else {
|
||||
currentInterface = DESKTOP;
|
||||
}
|
||||
}
|
||||
|
||||
return currentInterface;
|
||||
}
|
||||
|
||||
public static void set(Interface interf) {
|
||||
if(currentInterface != null && interf != currentInterface) {
|
||||
throw new IllegalStateException("Interface already set to " + currentInterface);
|
||||
}
|
||||
|
||||
currentInterface = interf;
|
||||
}
|
||||
}
|
||||
+10
-115
@@ -1,9 +1,8 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.control.WalletIcon;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
@@ -12,40 +11,28 @@ 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.instance.InstanceException;
|
||||
import com.sparrowwallet.sparrow.instance.InstanceList;
|
||||
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.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MainApp extends Application {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.6.6";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
||||
public class SparrowDesktop extends Application {
|
||||
private Stage mainStage;
|
||||
|
||||
private static SparrowInstance sparrowInstance;
|
||||
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
|
||||
if(e instanceof IndexOutOfBoundsException && Arrays.stream(e.getStackTrace()).anyMatch(element -> element.getClassName().equals("javafx.scene.chart.BarChart"))) {
|
||||
LoggerFactory.getLogger(MainApp.class).debug("Exception in thread \"" + t.getName() + "\"", e);;
|
||||
LoggerFactory.getLogger(SparrowWallet.class).debug("Exception in thread \"" + t.getName() + "\"", e);;
|
||||
} else {
|
||||
LoggerFactory.getLogger(MainApp.class).error("Exception in thread \"" + t.getName() + "\"", e);
|
||||
LoggerFactory.getLogger(SparrowWallet.class).error("Exception in thread \"" + t.getName() + "\"", e);
|
||||
}
|
||||
});
|
||||
super.init();
|
||||
@@ -58,6 +45,7 @@ public class MainApp extends Application {
|
||||
GlyphFontRegistry.register(new FontAwesome5());
|
||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
||||
URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null);
|
||||
|
||||
AppServices.initialize(this);
|
||||
|
||||
@@ -77,7 +65,7 @@ public class MainApp extends Application {
|
||||
} else if(Network.get() == Network.MAINNET) {
|
||||
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
|
||||
List<PublicElectrumServer> servers = PublicElectrumServer.getServers();
|
||||
Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getUrl());
|
||||
Config.get().setPublicElectrumServer(servers.get(new Random().nextInt(servers.size())).getServer());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,102 +117,9 @@ public class MainApp extends Application {
|
||||
Config.get().setAppWidth(mainStage.getWidth());
|
||||
Config.get().setAppHeight(mainStage.getHeight());
|
||||
mainStage.close();
|
||||
if(sparrowInstance != null) {
|
||||
sparrowInstance.freeLock();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] argv) {
|
||||
Args args = new Args();
|
||||
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
|
||||
jCommander.parse(argv);
|
||||
if(args.help) {
|
||||
jCommander.usage();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
if(args.level != null) {
|
||||
Drongo.setRootLogLevel(args.level);
|
||||
}
|
||||
|
||||
if(args.dir != null) {
|
||||
System.setProperty(APP_HOME_PROPERTY, args.dir);
|
||||
getLogger().info("Using configured Sparrow home folder of " + args.dir);
|
||||
}
|
||||
|
||||
if(args.network != null) {
|
||||
Network.set(args.network);
|
||||
} else {
|
||||
String envNetwork = System.getenv(NETWORK_ENV_PROPERTY);
|
||||
if(envNetwork != null) {
|
||||
try {
|
||||
Network.set(Network.valueOf(envNetwork.toUpperCase(Locale.ROOT)));
|
||||
} catch(Exception e) {
|
||||
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
|
||||
if(testnetFlag.exists()) {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||
if(signetFlag.exists()) {
|
||||
Network.set(Network.SIGNET);
|
||||
}
|
||||
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
getLogger().info("Using " + Network.get() + " configuration");
|
||||
}
|
||||
|
||||
List<String> fileUriArguments = jCommander.getUnknownOptions();
|
||||
|
||||
try {
|
||||
sparrowInstance = new SparrowInstance(fileUriArguments);
|
||||
sparrowInstance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
||||
} catch(InstanceException e) {
|
||||
getLogger().error("Could not access application lock", e);
|
||||
}
|
||||
|
||||
if(!fileUriArguments.isEmpty()) {
|
||||
AppServices.parseFileUriArguments(fileUriArguments);
|
||||
}
|
||||
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||
SLF4JBridgeHandler.install();
|
||||
com.sun.javafx.application.LauncherImpl.launchApplication(MainApp.class, MainAppPreloader.class, argv);
|
||||
}
|
||||
|
||||
private static Logger getLogger() {
|
||||
return LoggerFactory.getLogger(MainApp.class);
|
||||
}
|
||||
|
||||
private static class SparrowInstance extends InstanceList {
|
||||
private final List<String> fileUriArguments;
|
||||
|
||||
public SparrowInstance(List<String> fileUriArguments) {
|
||||
super(MainApp.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
|
||||
this.fileUriArguments = fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void receiveMessageList(List<String> messageList) {
|
||||
if(messageList != null && !messageList.isEmpty()) {
|
||||
AppServices.parseFileUriArguments(messageList);
|
||||
AppServices.openFileUriArguments(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> sendMessageList() {
|
||||
return fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeExit() {
|
||||
getLogger().info("Opening files/URIs in already running instance, exiting...");
|
||||
SparrowWallet.Instance instance = SparrowWallet.getSparrowInstance();
|
||||
if(instance != null) {
|
||||
instance.freeLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.instance.InstanceException;
|
||||
import com.sparrowwallet.sparrow.instance.InstanceList;
|
||||
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||
import com.sun.javafx.application.PlatformImpl;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public class SparrowWallet {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.7.4";
|
||||
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";
|
||||
|
||||
private static Instance instance;
|
||||
|
||||
public static void main(String[] argv) {
|
||||
Args args = new Args();
|
||||
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
|
||||
jCommander.parse(argv);
|
||||
if(args.help) {
|
||||
jCommander.usage();
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
if(args.version) {
|
||||
System.out.println("Sparrow Wallet " + APP_VERSION);
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
if(args.level != null) {
|
||||
Drongo.setRootLogLevel(args.level);
|
||||
}
|
||||
|
||||
if(args.dir != null) {
|
||||
System.setProperty(APP_HOME_PROPERTY, args.dir);
|
||||
getLogger().info("Using configured Sparrow home folder of " + args.dir);
|
||||
}
|
||||
|
||||
if(args.network != null) {
|
||||
Network.set(args.network);
|
||||
} else {
|
||||
String envNetwork = System.getenv(NETWORK_ENV_PROPERTY);
|
||||
if(envNetwork != null) {
|
||||
try {
|
||||
Network.set(Network.valueOf(envNetwork.toUpperCase(Locale.ROOT)));
|
||||
} catch(Exception e) {
|
||||
getLogger().warn("Invalid " + NETWORK_ENV_PROPERTY + " property: " + envNetwork);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
|
||||
if(testnetFlag.exists()) {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||
if(signetFlag.exists()) {
|
||||
Network.set(Network.SIGNET);
|
||||
}
|
||||
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
getLogger().info("Using " + Network.get() + " configuration");
|
||||
}
|
||||
|
||||
List<String> fileUriArguments = jCommander.getUnknownOptions();
|
||||
|
||||
try {
|
||||
instance = new Instance(fileUriArguments);
|
||||
instance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
||||
} catch(InstanceException e) {
|
||||
getLogger().error("Could not access application lock", e);
|
||||
}
|
||||
|
||||
if(!fileUriArguments.isEmpty()) {
|
||||
AppServices.parseFileUriArguments(fileUriArguments);
|
||||
}
|
||||
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||
SLF4JBridgeHandler.install();
|
||||
|
||||
if(args.terminal) {
|
||||
Interface.set(Interface.TERMINAL);
|
||||
}
|
||||
|
||||
try {
|
||||
if(Interface.get() == Interface.TERMINAL) {
|
||||
PlatformImpl.setTaskbarApplication(false);
|
||||
Drongo.removeRootLogAppender("STDOUT");
|
||||
com.sun.javafx.application.LauncherImpl.launchApplication(SparrowTerminal.class, SparrowWalletPreloader.class, argv);
|
||||
} else {
|
||||
com.sun.javafx.application.LauncherImpl.launchApplication(SparrowDesktop.class, SparrowWalletPreloader.class, argv);
|
||||
}
|
||||
} catch(UnsupportedOperationException e) {
|
||||
Drongo.removeRootLogAppender("STDOUT");
|
||||
getLogger().error("Unable to launch application", e);
|
||||
System.out.println("No display detected. Use Sparrow Server on a headless (no display) system.");
|
||||
|
||||
try {
|
||||
if(instance != null) {
|
||||
instance.freeLock();
|
||||
}
|
||||
} catch(InstanceException instanceException) {
|
||||
getLogger().error("Unable to free instance lock", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Instance getSparrowInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static Logger getLogger() {
|
||||
return LoggerFactory.getLogger(SparrowWallet.class);
|
||||
}
|
||||
|
||||
public static class Instance extends InstanceList {
|
||||
private final List<String> fileUriArguments;
|
||||
|
||||
public Instance(List<String> fileUriArguments) {
|
||||
super(SparrowWallet.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
|
||||
this.fileUriArguments = fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void receiveMessageList(List<String> messageList) {
|
||||
if(messageList != null && !messageList.isEmpty()) {
|
||||
AppServices.parseFileUriArguments(messageList);
|
||||
AppServices.openFileUriArguments(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> sendMessageList() {
|
||||
return fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeExit() {
|
||||
getLogger().info("Opening files/URIs in already running instance, exiting...");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@ package com.sparrowwallet.sparrow;
|
||||
import javafx.application.Preloader;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class MainAppPreloader extends Preloader {
|
||||
public class SparrowWalletPreloader extends Preloader {
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
com.sun.glass.ui.Application.GetApplication().setName("Sparrow");
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.LogHandler;
|
||||
import com.sparrowwallet.sparrow.event.TorStatusEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
public class TorLogHandler implements LogHandler {
|
||||
private static final Logger log = LoggerFactory.getLogger(TorLogHandler.class);
|
||||
|
||||
@Override
|
||||
public void handleLog(String threadName, Level level, String message, String loggerName, long timestamp, StackTraceElement[] callerData) {
|
||||
log.debug(message);
|
||||
EventManager.get().post(new TorStatusEvent(message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum UnitFormat {
|
||||
DOT {
|
||||
private final DecimalFormat btcFormat = new DecimalFormat("0", getDecimalFormatSymbols());
|
||||
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
|
||||
|
||||
public DecimalFormat getBtcFormat() {
|
||||
btcFormat.setMaximumFractionDigits(8);
|
||||
return btcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getSatsFormat() {
|
||||
return satsFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getTableBtcFormat() {
|
||||
return tableBtcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getCurrencyFormat() {
|
||||
return currencyFormat;
|
||||
}
|
||||
|
||||
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
|
||||
symbols.setDecimalSeparator('.');
|
||||
symbols.setGroupingSeparator(',');
|
||||
return symbols;
|
||||
}
|
||||
},
|
||||
COMMA {
|
||||
private final DecimalFormat btcFormat = new DecimalFormat("0", getDecimalFormatSymbols());
|
||||
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
|
||||
|
||||
public DecimalFormat getBtcFormat() {
|
||||
btcFormat.setMaximumFractionDigits(8);
|
||||
return btcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getSatsFormat() {
|
||||
return satsFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getTableBtcFormat() {
|
||||
return tableBtcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getCurrencyFormat() {
|
||||
return currencyFormat;
|
||||
}
|
||||
|
||||
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
|
||||
symbols.setDecimalSeparator(',');
|
||||
symbols.setGroupingSeparator('.');
|
||||
return symbols;
|
||||
}
|
||||
};
|
||||
|
||||
public abstract DecimalFormatSymbols getDecimalFormatSymbols();
|
||||
|
||||
public abstract DecimalFormat getBtcFormat();
|
||||
|
||||
public abstract DecimalFormat getSatsFormat();
|
||||
|
||||
public abstract DecimalFormat getTableBtcFormat();
|
||||
|
||||
public abstract DecimalFormat getCurrencyFormat();
|
||||
|
||||
public String formatBtcValue(Long amount) {
|
||||
return getBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
}
|
||||
|
||||
public String tableFormatBtcValue(Long amount) {
|
||||
return getTableBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
}
|
||||
|
||||
public String formatSatsValue(Long amount) {
|
||||
return getSatsFormat().format(amount);
|
||||
}
|
||||
|
||||
public String formatCurrencyValue(double amount) {
|
||||
return getCurrencyFormat().format(amount);
|
||||
}
|
||||
|
||||
public String getGroupingSeparator() {
|
||||
return Character.toString(getDecimalFormatSymbols().getGroupingSeparator());
|
||||
}
|
||||
|
||||
public String getDecimalSeparator() {
|
||||
return Character.toString(getDecimalFormatSymbols().getDecimalSeparator());
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
if(utxoEntry != null) {
|
||||
Address address = addressStatus.getAddress();
|
||||
setText(address.toString());
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode()), false));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.util.*;
|
||||
public class AddressTreeTable extends CoinTreeTable {
|
||||
public void initialize(NodeEntry rootEntry) {
|
||||
getStyleClass().add("address-treetable");
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
String address = rootEntry.getAddress().toString();
|
||||
updateAll(rootEntry);
|
||||
@@ -114,7 +114,7 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
}
|
||||
|
||||
public void updateAll(NodeEntry rootEntry) {
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
setRoot(rootItem);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
@@ -35,8 +36,7 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||
getData().add(balanceSeries);
|
||||
update(walletTransactionsEntry);
|
||||
|
||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||
setBitcoinUnit(walletTransactionsEntry.getWallet(), unit);
|
||||
setUnitFormat(walletTransactionsEntry.getWallet(), Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void update(WalletTransactionsEntry walletTransactionsEntry) {
|
||||
@@ -116,12 +116,16 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||
}
|
||||
}
|
||||
|
||||
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
|
||||
public void setUnitFormat(Wallet wallet, UnitFormat format, BitcoinUnit unit) {
|
||||
if(format == null) {
|
||||
format = UnitFormat.DOT;
|
||||
}
|
||||
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = wallet.getAutoUnit();
|
||||
}
|
||||
|
||||
NumberAxis yaxis = (NumberAxis)getYAxis();
|
||||
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, unit));
|
||||
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreCardImport;
|
||||
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.smartcardio.CardException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
public class CardImportPane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardImportPane.class);
|
||||
|
||||
private final KeystoreCardImport importer;
|
||||
private final List<ChildNumber> derivation;
|
||||
protected Button importButton;
|
||||
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
||||
|
||||
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) {
|
||||
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
this.importer = importer;
|
||||
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
importButton = new Button("Import");
|
||||
Glyph tapGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI);
|
||||
tapGlyph.setFontSize(12);
|
||||
importButton.setGraphic(tapGlyph);
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importCard();
|
||||
});
|
||||
return importButton;
|
||||
}
|
||||
|
||||
private void importCard() {
|
||||
if(pin.get().length() < 6) {
|
||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||
setContent(getPinEntry());
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
importButton.setDisable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
StringProperty messageProperty = new SimpleStringProperty();
|
||||
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setDescription(newValue));
|
||||
});
|
||||
|
||||
try {
|
||||
if(!importer.isInitialized()) {
|
||||
setDescription("Card not initialized");
|
||||
setContent(getInitializationPanel(messageProperty));
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
return;
|
||||
}
|
||||
} catch(CardException e) {
|
||||
setError("Card Error", e.getMessage());
|
||||
importButton.setDisable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty);
|
||||
cardImportService.setOnSucceeded(event -> {
|
||||
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
|
||||
});
|
||||
cardImportService.setOnFailed(event -> {
|
||||
Throwable rootCause = Throwables.getRootCause(event.getSource().getException());
|
||||
if(rootCause instanceof CardAuthorizationException) {
|
||||
setError(rootCause.getMessage(), null);
|
||||
setContent(getPinEntry());
|
||||
} else {
|
||||
log.error("Error importing keystore from card", event.getSource().getException());
|
||||
setError("Import Error", rootCause.getMessage());
|
||||
}
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
cardImportService.start();
|
||||
}
|
||||
|
||||
private Node getInitializationPanel(StringProperty messageProperty) {
|
||||
VBox initTypeBox = new VBox(5);
|
||||
RadioButton automatic = new RadioButton("Automatic (Recommended)");
|
||||
RadioButton advanced = new RadioButton("Advanced");
|
||||
TextField entropy = new TextField();
|
||||
entropy.setPromptText("Enter input for user entropy");
|
||||
entropy.setDisable(true);
|
||||
|
||||
ToggleGroup toggleGroup = new ToggleGroup();
|
||||
automatic.setToggleGroup(toggleGroup);
|
||||
advanced.setToggleGroup(toggleGroup);
|
||||
automatic.setSelected(true);
|
||||
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
entropy.setDisable(newValue == automatic);
|
||||
});
|
||||
|
||||
initTypeBox.getChildren().addAll(automatic, advanced, entropy);
|
||||
|
||||
Button initializeButton = new Button("Initialize");
|
||||
initializeButton.setDefaultButton(true);
|
||||
initializeButton.setOnAction(event -> {
|
||||
initializeButton.setDisable(true);
|
||||
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
|
||||
CardInitializationService cardInitializationService = new CardInitializationService(importer, pin.get(), chainCode, messageProperty);
|
||||
cardInitializationService.setOnSucceeded(successEvent -> {
|
||||
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
|
||||
setDescription("Leave card on reader");
|
||||
setExpanded(false);
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
cardInitializationService.setOnFailed(failEvent -> {
|
||||
Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
|
||||
if(rootCause instanceof CardAuthorizationException) {
|
||||
setError(rootCause.getMessage(), null);
|
||||
setContent(getPinEntry());
|
||||
importButton.setDisable(false);
|
||||
} else {
|
||||
log.error("Error initializing card", failEvent.getSource().getException());
|
||||
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
|
||||
initializeButton.setDisable(false);
|
||||
}
|
||||
});
|
||||
cardInitializationService.start();
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox(20);
|
||||
contentBox.getChildren().addAll(initTypeBox, initializeButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
HBox.setHgrow(initTypeBox, Priority.ALWAYS);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private Node getPinEntry() {
|
||||
VBox vBox = new VBox();
|
||||
|
||||
CustomPasswordField pinField = new ViewPasswordField();
|
||||
pinField.setPromptText("PIN Code");
|
||||
importButton.setDefaultButton(true);
|
||||
pin.bind(pinField.textProperty());
|
||||
HBox.setHgrow(pinField, Priority.ALWAYS);
|
||||
Platform.runLater(pinField::requestFocus);
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().add(pinField);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(50);
|
||||
|
||||
vBox.getChildren().add(contentBox);
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
public static class CardInitializationService extends Service<Void> {
|
||||
private final KeystoreCardImport cardImport;
|
||||
private final String pin;
|
||||
private final byte[] chainCode;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public CardInitializationService(KeystoreCardImport cardImport, String pin, byte[] chainCode, StringProperty messageProperty) {
|
||||
this.cardImport = cardImport;
|
||||
this.pin = pin;
|
||||
this.chainCode = chainCode;
|
||||
this.messageProperty = messageProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
cardImport.initialize(pin, chainCode, messageProperty);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class CardImportService extends Service<Keystore> {
|
||||
private final KeystoreCardImport cardImport;
|
||||
private final String pin;
|
||||
private final List<ChildNumber> derivation;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public CardImportService(KeystoreCardImport cardImport, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
this.cardImport = cardImport;
|
||||
this.pin = pin;
|
||||
this.derivation = derivation;
|
||||
this.messageProperty = messageProperty;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Keystore> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Keystore call() throws Exception {
|
||||
return cardImport.getKeystore(pin, derivation, messageProperty);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.scene.control.*;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
public class CardPinDialog extends Dialog<CardPinDialog.CardPinChange> {
|
||||
private final CustomPasswordField existingPin;
|
||||
private final CustomPasswordField newPin;
|
||||
private final CustomPasswordField newPinConfirm;
|
||||
private final CheckBox backupFirst;
|
||||
private final ButtonType okButtonType;
|
||||
|
||||
public CardPinDialog() {
|
||||
this.existingPin = new ViewPasswordField();
|
||||
this.newPin = new ViewPasswordField();
|
||||
this.newPinConfirm = new ViewPasswordField();
|
||||
this.backupFirst = new CheckBox();
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("Change Card PIN");
|
||||
dialogPane.setHeaderText("Enter the current PIN, and then the new PIN twice. PIN must be between 6 and 32 digits.");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(260);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Glyph lock = new Glyph("FontAwesome", FontAwesome.Glyph.LOCK);
|
||||
lock.setFontSize(50);
|
||||
dialogPane.setGraphic(lock);
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
fieldset.setSpacing(10);
|
||||
|
||||
Field currentField = new Field();
|
||||
currentField.setText("Current PIN:");
|
||||
currentField.getInputs().add(existingPin);
|
||||
|
||||
Field newField = new Field();
|
||||
newField.setText("New PIN:");
|
||||
newField.getInputs().add(newPin);
|
||||
|
||||
Field confirmField = new Field();
|
||||
confirmField.setText("Confirm new PIN:");
|
||||
confirmField.getInputs().add(newPinConfirm);
|
||||
|
||||
Field backupField = new Field();
|
||||
backupField.setText("Backup First:");
|
||||
backupField.getInputs().add(backupFirst);
|
||||
|
||||
fieldset.getChildren().addAll(currentField, newField, confirmField, backupField);
|
||||
form.getChildren().add(fieldset);
|
||||
dialogPane.setContent(form);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater( () -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(existingPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", existingPin.getText().length() < 6 || existingPin.getText().length() > 32));
|
||||
validationSupport.registerValidator(newPin, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Incorrect PIN length", newPin.getText().length() < 6 || newPin.getText().length() > 32));
|
||||
validationSupport.registerValidator(newPinConfirm, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "PIN confirmation does not match", !newPinConfirm.getText().equals(newPin.getText())));
|
||||
});
|
||||
|
||||
okButtonType = new javafx.scene.control.ButtonType("Change", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||
okButton.setPrefWidth(130);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> existingPin.getText().length() < 6 || existingPin.getText().length() > 32
|
||||
|| newPin.getText().length() < 6 || newPin.getText().length() > 32
|
||||
|| !newPin.getText().equals(newPinConfirm.getText()),
|
||||
existingPin.textProperty(), newPin.textProperty(), newPinConfirm.textProperty());
|
||||
okButton.disableProperty().bind(isInvalid);
|
||||
|
||||
Platform.runLater(existingPin::requestFocus);
|
||||
setResultConverter(dialogButton -> dialogButton == okButtonType ? new CardPinChange(existingPin.getText(), newPin.getText(), backupFirst.isSelected()) : null);
|
||||
}
|
||||
|
||||
public record CardPinChange(String currentPin, String newPin, boolean backupFirst) { }
|
||||
}
|
||||
@@ -1,28 +1,31 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.text.ParseException;
|
||||
|
||||
final class CoinAxisFormatter extends StringConverter<Number> {
|
||||
private final UnitFormat unitFormat;
|
||||
private final BitcoinUnit bitcoinUnit;
|
||||
|
||||
public CoinAxisFormatter(NumberAxis axis, BitcoinUnit unit) {
|
||||
public CoinAxisFormatter(NumberAxis axis, UnitFormat format, BitcoinUnit unit) {
|
||||
this.unitFormat = format;
|
||||
this.bitcoinUnit = unit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
Double value = bitcoinUnit.getValue(object.longValue());
|
||||
return CoinTextFormatter.COIN_FORMAT.format(value);
|
||||
return new CoinTextFormatter(unitFormat).getCoinFormat().format(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
try {
|
||||
Number number = CoinTextFormatter.COIN_FORMAT.parse(string);
|
||||
Number number = new CoinTextFormatter(unitFormat).getCoinFormat().parse(string);
|
||||
return bitcoinUnit.getSatsValue(number.doubleValue());
|
||||
} catch (ParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
||||
@@ -2,30 +2,35 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.scene.control.*;
|
||||
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;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
public static final DecimalFormat TABLE_BTC_FORMAT = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsListener {
|
||||
private final CoinTooltip tooltip;
|
||||
private final CoinContextMenu contextMenu;
|
||||
|
||||
private final Tooltip tooltip;
|
||||
private IntegerProperty confirmationsProperty;
|
||||
|
||||
public CoinCell() {
|
||||
super();
|
||||
tooltip = new Tooltip();
|
||||
tooltip = new CoinTooltip();
|
||||
tooltip.setShowDelay(Duration.millis(500));
|
||||
contextMenu = new CoinContextMenu();
|
||||
getStyleClass().add("coin-cell");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
getStyleClass().add("number-field");
|
||||
@@ -40,34 +45,32 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
setContextMenu(null);
|
||||
} else {
|
||||
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
||||
EntryCell.applyRowStyles(this, entry);
|
||||
|
||||
CoinTreeTable coinTreeTable = (CoinTreeTable)getTreeTableView();
|
||||
UnitFormat format = coinTreeTable.getUnitFormat();
|
||||
BitcoinUnit unit = coinTreeTable.getBitcoinUnit();
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", amount.longValue());
|
||||
DecimalFormat decimalFormat = (amount.longValue() == 0L ? CoinLabel.getBTCFormat() : TABLE_BTC_FORMAT);
|
||||
String satsValue = format.formatSatsValue(amount.longValue());
|
||||
DecimalFormat decimalFormat = (amount.longValue() == 0L ? format.getBtcFormat() : format.getTableBtcFormat());
|
||||
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setText(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
|
||||
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
|
||||
setText(btcValue);
|
||||
} else {
|
||||
tooltip.setText(btcValue + " " + BitcoinUnit.BTC.getLabel());
|
||||
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
|
||||
setText(satsValue);
|
||||
}
|
||||
setTooltip(tooltip);
|
||||
String tooltipValue = tooltip.getText();
|
||||
contextMenu.updateAmount(amount);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
if(entry instanceof TransactionEntry) {
|
||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||
tooltip.setText(tooltipValue + " (" + transactionEntry.getConfirmationsDescription() + ")");
|
||||
|
||||
transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> {
|
||||
tooltip.setText(tooltipValue + " (" + transactionEntry.getConfirmationsDescription() + ")");
|
||||
});
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
tooltip.showConfirmations(transactionEntry.confirmationsProperty());
|
||||
|
||||
if(transactionEntry.isConfirming()) {
|
||||
ConfirmationProgressIndicator arc = new ConfirmationProgressIndicator(transactionEntry.getConfirmations());
|
||||
@@ -97,4 +100,97 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegerProperty getConfirmationsProperty() {
|
||||
if(confirmationsProperty == null) {
|
||||
confirmationsProperty = new SimpleIntegerProperty();
|
||||
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
|
||||
getStyleClass().remove("confirming");
|
||||
confirmationsProperty.unbind();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return confirmationsProperty;
|
||||
}
|
||||
|
||||
private static final class CoinTooltip extends Tooltip {
|
||||
private final IntegerProperty confirmationsProperty = new SimpleIntegerProperty();
|
||||
private boolean showConfirmations;
|
||||
private String value;
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
setTooltipText();
|
||||
}
|
||||
|
||||
public void showConfirmations(IntegerProperty txEntryConfirmationsProperty) {
|
||||
showConfirmations = true;
|
||||
|
||||
int confirmations = txEntryConfirmationsProperty.get();
|
||||
if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||
confirmationsProperty.bind(txEntryConfirmationsProperty);
|
||||
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
|
||||
setTooltipText();
|
||||
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||
confirmationsProperty.unbind();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
confirmationsProperty.unbind();
|
||||
confirmationsProperty.set(confirmations);
|
||||
}
|
||||
|
||||
setTooltipText();
|
||||
}
|
||||
|
||||
private void setTooltipText() {
|
||||
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
|
||||
}
|
||||
|
||||
public String getConfirmationsDescription() {
|
||||
int confirmations = confirmationsProperty.get();
|
||||
if(confirmations == 0) {
|
||||
return "Unconfirmed in mempool";
|
||||
} else if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s");
|
||||
} else {
|
||||
return BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM + "+ confirmations";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class CoinContextMenu extends ContextMenu {
|
||||
private Number amount;
|
||||
|
||||
public void updateAmount(Number amount) {
|
||||
if(amount.equals(this.amount)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.amount = amount;
|
||||
getItems().clear();
|
||||
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||
copySatsValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(amount.toString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
MenuItem copyBtcValue = new MenuItem("Copy Value in BTC");
|
||||
copyBtcValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
content.putString(format.formatBtcValue(amount.longValue()));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(copySatsValue, copyBtcValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
@@ -12,13 +12,7 @@ import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
public class CoinLabel extends Label {
|
||||
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
private final CoinContextMenu contextMenu;
|
||||
@@ -58,8 +52,9 @@ public class CoinLabel extends Label {
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
|
||||
String btcValue = BTC_FORMAT.format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
String satsValue = format.formatSatsValue(value) + " sats";
|
||||
String btcValue = format.formatBtcValue(value) + " BTC";
|
||||
|
||||
BitcoinUnit unit = bitcoinUnit;
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
@@ -89,16 +84,12 @@ public class CoinLabel extends Label {
|
||||
copyBtcValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(BTC_FORMAT.format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
content.putString(format.formatBtcValue(getValue()));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(copySatsValue, copyBtcValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static DecimalFormat getBTCFormat() {
|
||||
BTC_FORMAT.setMaximumFractionDigits(8);
|
||||
return BTC_FORMAT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
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.Locale;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CoinTextFormatter extends TextFormatter<String> {
|
||||
private static final Pattern COIN_VALIDATION = Pattern.compile("[\\d,]*(\\.\\d{0,8})?");
|
||||
public static final DecimalFormat COIN_FORMAT = new DecimalFormat("###,###.########", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
public CoinTextFormatter(UnitFormat unitFormat) {
|
||||
super(new CoinFilter(unitFormat));
|
||||
}
|
||||
|
||||
public CoinTextFormatter() {
|
||||
super(new CoinFilter());
|
||||
public UnitFormat getUnitFormat() {
|
||||
return ((CoinFilter)getFilter()).unitFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getCoinFormat() {
|
||||
return ((CoinFilter)getFilter()).coinFormat;
|
||||
}
|
||||
|
||||
private static class CoinFilter implements UnaryOperator<Change> {
|
||||
private final UnitFormat unitFormat;
|
||||
private final DecimalFormat coinFormat;
|
||||
private final Pattern coinValidation;
|
||||
|
||||
public CoinFilter(UnitFormat unitFormat) {
|
||||
this.unitFormat = unitFormat == null ? UnitFormat.DOT : unitFormat;
|
||||
this.coinFormat = new DecimalFormat("###,###.########", unitFormat.getDecimalFormatSymbols());
|
||||
this.coinValidation = Pattern.compile("[\\d" + Pattern.quote(unitFormat.getGroupingSeparator()) + "]*(" + Pattern.quote(unitFormat.getDecimalSeparator()) + "\\d{0,8})?");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Change apply(Change change) {
|
||||
String oldText = change.getControlText();
|
||||
@@ -30,17 +45,17 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||
|
||||
String noFractionCommaText = newText;
|
||||
int commasRemoved = 0;
|
||||
int dotIndex = newText.indexOf(".");
|
||||
int dotIndex = newText.indexOf(unitFormat.getDecimalSeparator());
|
||||
if(dotIndex > -1) {
|
||||
noFractionCommaText = newText.substring(0, dotIndex) + newText.substring(dotIndex).replaceAll(",", "");
|
||||
noFractionCommaText = newText.substring(0, dotIndex) + newText.substring(dotIndex).replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "");
|
||||
commasRemoved = newText.length() - noFractionCommaText.length();
|
||||
}
|
||||
|
||||
if(!COIN_VALIDATION.matcher(noFractionCommaText).matches()) {
|
||||
if(!coinValidation.matcher(noFractionCommaText).matches()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if(",".equals(change.getText())) {
|
||||
if(unitFormat.getGroupingSeparator().equals(change.getText())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -48,20 +63,20 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||
return change;
|
||||
}
|
||||
|
||||
if(change.isDeleted() && ",".equals(deleted) && change.getRangeStart() > 0) {
|
||||
if(change.isDeleted() && unitFormat.getGroupingSeparator().equals(deleted) && change.getRangeStart() > 0) {
|
||||
noFractionCommaText = noFractionCommaText.substring(0, change.getRangeStart() - 1) + noFractionCommaText.substring(change.getRangeEnd() - 1);
|
||||
}
|
||||
|
||||
try {
|
||||
Number value = COIN_FORMAT.parse(noFractionCommaText);
|
||||
String correct = COIN_FORMAT.format(value.doubleValue());
|
||||
Number value = coinFormat.parse(noFractionCommaText);
|
||||
String correct = coinFormat.format(value.doubleValue());
|
||||
|
||||
String compare = newText;
|
||||
if(compare.contains(".") && compare.endsWith("0")) {
|
||||
if(compare.contains(unitFormat.getDecimalSeparator()) && compare.endsWith("0")) {
|
||||
compare = compare.replaceAll("0*$", "");
|
||||
}
|
||||
|
||||
if(compare.endsWith(".")) {
|
||||
if(compare.endsWith(unitFormat.getDecimalSeparator())) {
|
||||
compare = compare.substring(0, compare.length() - 1);
|
||||
}
|
||||
|
||||
@@ -79,11 +94,11 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||
|
||||
if(correct.length() != newText.length()) {
|
||||
String postCorrect = correct.substring(Math.min(change.getCaretPosition(), correct.length()));
|
||||
int commasAfter = postCorrect.length() - postCorrect.replace(",", "").length();
|
||||
int caretShift = change.isDeleted() && ".".equals(deleted) ? commasAfter : 0;
|
||||
int commasAfter = postCorrect.length() - postCorrect.replace(unitFormat.getGroupingSeparator(), "").length();
|
||||
int caretShift = change.isDeleted() && unitFormat.getDecimalSeparator().equals(deleted) ? commasAfter : 0;
|
||||
|
||||
int caret = change.getCaretPosition() + (correct.length() - newText.length() - caretShift) + commasRemoved;
|
||||
if(caret >= 0) {
|
||||
if(caret >= 0 && caret <= change.getControlNewText().length()) {
|
||||
change.setCaretPosition(caret);
|
||||
change.setAnchor(caret);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||
@@ -26,25 +27,36 @@ import java.util.Optional;
|
||||
|
||||
public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
private UnitFormat unitFormat;
|
||||
|
||||
public BitcoinUnit getBitcoinUnit() {
|
||||
return bitcoinUnit;
|
||||
}
|
||||
|
||||
public void setBitcoinUnit(BitcoinUnit bitcoinUnit) {
|
||||
this.bitcoinUnit = bitcoinUnit;
|
||||
public UnitFormat getUnitFormat() {
|
||||
return unitFormat;
|
||||
}
|
||||
|
||||
public void setBitcoinUnit(Wallet wallet) {
|
||||
setBitcoinUnit(wallet, Config.get().getBitcoinUnit());
|
||||
public void setUnitFormat(Wallet wallet) {
|
||||
setUnitFormat(wallet, Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
|
||||
public void setUnitFormat(Wallet wallet, UnitFormat format) {
|
||||
setUnitFormat(wallet, format, Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void setUnitFormat(Wallet wallet, UnitFormat format, BitcoinUnit unit) {
|
||||
if(format == null) {
|
||||
format = UnitFormat.DOT;
|
||||
}
|
||||
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = wallet.getAutoUnit();
|
||||
}
|
||||
|
||||
boolean changed = (bitcoinUnit != unit);
|
||||
boolean changed = (unitFormat != format);
|
||||
changed |= (bitcoinUnit != unit);
|
||||
this.unitFormat = format;
|
||||
this.bitcoinUnit = unit;
|
||||
|
||||
if(changed && !getChildren().isEmpty()) {
|
||||
@@ -81,7 +93,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
Hyperlink hyperlink = new Hyperlink();
|
||||
hyperlink.setTranslateY(30);
|
||||
hyperlink.setOnAction(event -> {
|
||||
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
|
||||
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate(), false);
|
||||
Optional<Date> optDate = dlg.showAndWait();
|
||||
if(optDate.isPresent()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
|
||||
public interface ConfirmationsListener {
|
||||
IntegerProperty getConfirmationsProperty();
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
@@ -11,8 +11,6 @@ import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class CopyableCoinLabel extends CopyableLabel {
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
@@ -24,7 +22,7 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
|
||||
public CopyableCoinLabel(String text) {
|
||||
super(text);
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new CoinContextMenu();
|
||||
}
|
||||
@@ -42,19 +40,23 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
refresh(Config.get().getBitcoinUnit());
|
||||
refresh(Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void refresh(BitcoinUnit bitcoinUnit) {
|
||||
setValueAsText(getValue(), bitcoinUnit);
|
||||
public void refresh(UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
|
||||
setValueAsText(getValue(), unitFormat, bitcoinUnit);
|
||||
}
|
||||
|
||||
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
|
||||
private void setValueAsText(Long value, UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
String satsValue = String.format(Locale.ENGLISH, "%,d", value) + " sats";
|
||||
String btcValue = CoinLabel.getBTCFormat().format(value.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
|
||||
if(unitFormat == null) {
|
||||
unitFormat = UnitFormat.DOT;
|
||||
}
|
||||
|
||||
String satsValue = unitFormat.formatSatsValue(value) + " sats";
|
||||
String btcValue = unitFormat.formatBtcValue(value) + " BTC";
|
||||
|
||||
BitcoinUnit unit = bitcoinUnit;
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
@@ -84,7 +86,8 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
copyBtcValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(CoinLabel.getBTCFormat().format((double)getValue() / Transaction.SATOSHIS_PER_BITCOIN));
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
content.putString(format.formatBtcValue(getValue()));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.lowagie.text.*;
|
||||
import com.lowagie.text.Font;
|
||||
import com.lowagie.text.Image;
|
||||
import com.lowagie.text.pdf.PdfWriter;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.UREncoder;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.PdfUtils;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
|
||||
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
private static final Logger log = LoggerFactory.getLogger(DescriptorQRDisplayDialog.class);
|
||||
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
|
||||
super(ur);
|
||||
|
||||
@@ -31,43 +16,11 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
dialogPane.getButtonTypes().add(pdfButtonType);
|
||||
|
||||
Button pdfButton = (Button)dialogPane.lookupButton(pdfButtonType);
|
||||
pdfButton.setGraphicTextGap(5);
|
||||
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
||||
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
savePdf(walletName, outputDescriptor, ur);
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur);
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
private void savePdf(String walletName, String outputDescriptor, UR ur) {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save PDF");
|
||||
fileChooser.setInitialFileName(walletName + ".pdf");
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
try(Document document = new Document()) {
|
||||
document.setMargins(36, 36, 48, 36);
|
||||
PdfWriter.getInstance(document, new FileOutputStream(file));
|
||||
document.open();
|
||||
|
||||
Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16, Color.BLACK);
|
||||
Chunk title = new Chunk("Output descriptor for " + walletName, titleFont);
|
||||
document.add(title);
|
||||
|
||||
UREncoder urEncoder = new UREncoder(ur, 2000, 10, 0);
|
||||
String fragment = urEncoder.nextPart();
|
||||
if(urEncoder.isSinglePart()) {
|
||||
Image image = Image.getInstance(SwingFXUtils.fromFXImage(getQrCode(fragment), null), Color.WHITE);
|
||||
document.add(image);
|
||||
}
|
||||
|
||||
Font descriptorFont = FontFactory.getFont(FontFactory.COURIER, 14, Color.BLACK);
|
||||
Paragraph descriptor = new Paragraph(outputDescriptor, descriptorFont);
|
||||
document.add(descriptor);
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating descriptor PDF", e);
|
||||
AppServices.showErrorDialog("Error creating PDF", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
|
||||
stackPane.getChildren().addAll(anchorPane, scanBox);
|
||||
|
||||
List<Device> devices = AppServices.getDevices();
|
||||
List<Device> devices = getDevices();
|
||||
if(devices == null || devices.isEmpty()) {
|
||||
scanButton.setDefaultButton(true);
|
||||
scanBox.setVisible(true);
|
||||
@@ -96,6 +96,10 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
|
||||
}
|
||||
|
||||
protected List<Device> getDevices() {
|
||||
return AppServices.getDevices();
|
||||
}
|
||||
|
||||
private void scan() {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
|
||||
+2
-2
@@ -9,11 +9,11 @@ import com.sparrowwallet.sparrow.io.Device;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class DeviceAddressDialog extends DeviceDialog<String> {
|
||||
public class DeviceDisplayAddressDialog extends DeviceDialog<String> {
|
||||
private final Wallet wallet;
|
||||
private final OutputDescriptor outputDescriptor;
|
||||
|
||||
public DeviceAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
|
||||
public DeviceDisplayAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
|
||||
super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
|
||||
this.wallet = wallet;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.DeviceAddressEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DeviceGetAddressDialog extends DeviceDialog<Address> {
|
||||
public DeviceGetAddressDialog(List<String> operationFingerprints) {
|
||||
super(operationFingerprints);
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(DevicePane.DeviceOperation.GET_ADDRESS, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void deviceAddress(DeviceAddressEvent event) {
|
||||
setResult(event.getAddress());
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,33 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.CardApi;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.WorkerStateEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
@@ -34,8 +42,10 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class DevicePane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(DevicePane.class);
|
||||
@@ -58,8 +68,12 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
private Button displayAddressButton;
|
||||
private Button signMessageButton;
|
||||
private Button discoverKeystoresButton;
|
||||
private ButtonBase getPrivateKeyButton;
|
||||
private Button getAddressButton;
|
||||
|
||||
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
|
||||
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
||||
private final StringProperty messageProperty = new SimpleStringProperty("");
|
||||
|
||||
private boolean defaultDevice;
|
||||
|
||||
@@ -83,13 +97,17 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
initialise(device);
|
||||
|
||||
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setDescription(newValue));
|
||||
});
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, importButton);
|
||||
}
|
||||
|
||||
public DevicePane(PSBT psbt, Device device, boolean defaultDevice) {
|
||||
public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = DeviceOperation.SIGN;
|
||||
this.wallet = null;
|
||||
this.wallet = wallet;
|
||||
this.psbt = psbt;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
@@ -106,6 +124,10 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
initialise(device);
|
||||
|
||||
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setDescription(newValue));
|
||||
});
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, signButton);
|
||||
}
|
||||
|
||||
@@ -152,6 +174,10 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
initialise(device);
|
||||
|
||||
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setDescription(newValue));
|
||||
});
|
||||
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
|
||||
}
|
||||
|
||||
@@ -178,6 +204,41 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
|
||||
}
|
||||
|
||||
public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
this.deviceOperation = deviceOperation;
|
||||
this.wallet = null;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
this.availableAccounts = null;
|
||||
|
||||
setDefaultStatus();
|
||||
showHideLink.setVisible(false);
|
||||
|
||||
ButtonBase button;
|
||||
if(deviceOperation == DeviceOperation.GET_PRIVATE_KEY) {
|
||||
createGetPrivateKeyButton();
|
||||
button = getPrivateKeyButton;
|
||||
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
|
||||
createGetAddressButton();
|
||||
button = getAddressButton;
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot construct device pane for operation " + deviceOperation);
|
||||
}
|
||||
|
||||
initialise(device);
|
||||
|
||||
messageProperty.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> setDescription(newValue));
|
||||
});
|
||||
|
||||
buttonBox.getChildren().add(button);
|
||||
}
|
||||
|
||||
private void initialise(Device device) {
|
||||
if(device.isNeedsPinSent()) {
|
||||
unlockButton.setDefaultButton(defaultDevice);
|
||||
@@ -201,7 +262,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void setDefaultStatus() {
|
||||
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : "Unlocked");
|
||||
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : device.isCard() ? "Leave card on reader" : "Unlocked");
|
||||
}
|
||||
|
||||
private void createUnlockButton() {
|
||||
@@ -321,6 +382,60 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
discoverKeystoresButton.setVisible(false);
|
||||
}
|
||||
|
||||
private void createGetPrivateKeyButton() {
|
||||
int currentSlot = 0;
|
||||
boolean initialized = true;
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), null);
|
||||
currentSlot = cardApi.getCurrentSlot();
|
||||
initialized = cardApi.isInitialized();
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
getPrivateKeyButton = currentSlot > 0 ? new SplitMenuButton() : new Button();
|
||||
getPrivateKeyButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
getPrivateKeyButton.setText("Get Private Key");
|
||||
getPrivateKeyButton.setOnAction(event -> {
|
||||
getPrivateKeyButton.setDisable(true);
|
||||
getPrivateKey(null);
|
||||
});
|
||||
|
||||
if(getPrivateKeyButton instanceof SplitMenuButton getPrivateKeyMenuButton) {
|
||||
int[] previousSlots = IntStream.range(0, currentSlot).toArray();
|
||||
for(int previousSlot : previousSlots) {
|
||||
MenuItem previousSlotItem = new MenuItem("Slot #" + previousSlot);
|
||||
previousSlotItem.setOnAction(event -> {
|
||||
getPrivateKeyButton.setDisable(true);
|
||||
getPrivateKey(previousSlot);
|
||||
});
|
||||
getPrivateKeyMenuButton.getItems().add(previousSlotItem);
|
||||
}
|
||||
if(initialized) {
|
||||
int finalSlot = currentSlot;
|
||||
MenuItem currentSlotItem = new MenuItem("Current Slot");
|
||||
currentSlotItem.setOnAction(event -> {
|
||||
getPrivateKeyButton.setDisable(true);
|
||||
getPrivateKey(finalSlot);
|
||||
});
|
||||
getPrivateKeyMenuButton.getItems().add(currentSlotItem);
|
||||
}
|
||||
}
|
||||
getPrivateKeyButton.managedProperty().bind(getPrivateKeyButton.visibleProperty());
|
||||
getPrivateKeyButton.setVisible(false);
|
||||
}
|
||||
|
||||
private void createGetAddressButton() {
|
||||
getAddressButton = new Button("Get Address");
|
||||
getAddressButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
getAddressButton.setOnAction(event -> {
|
||||
getAddressButton.setDisable(true);
|
||||
getAddress();
|
||||
});
|
||||
getAddressButton.managedProperty().bind(getAddressButton.visibleProperty());
|
||||
getAddressButton.setVisible(false);
|
||||
}
|
||||
|
||||
private void unlock(Device device) {
|
||||
if(device.getModel().requiresPinPrompt()) {
|
||||
promptPin();
|
||||
@@ -554,7 +669,36 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void importKeystore(List<ChildNumber> derivation) {
|
||||
if(device.getFingerprint() == null) {
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||
if(!cardApi.isInitialized()) {
|
||||
if(pin.get().length() < 6) {
|
||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||
setContent(getCardPinEntry(importButton));
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
importButton.setDisable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setDescription("Card not initialized");
|
||||
setContent(getCardInitializationPanel(cardApi, importButton, DeviceOperation.IMPORT));
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
|
||||
handleCardOperation(importService, importButton, "Import", true, event -> {
|
||||
importKeystore(derivation, importService.getValue());
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Import Error: " + e.getMessage(), e);
|
||||
setError("Import Error", e.getMessage());
|
||||
importButton.setDisable(false);
|
||||
}
|
||||
} else if(device.getFingerprint() == null) {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(passphrase.get());
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
List<Device> devices = enumerateService.getValue();
|
||||
@@ -591,18 +735,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
|
||||
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
|
||||
wallet.setName(device.getModel().toDisplayString());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
importKeystore(derivation, keystore);
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve xpub", e.getMessage());
|
||||
}
|
||||
@@ -616,20 +749,74 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
getXpubService.start();
|
||||
}
|
||||
|
||||
private void importKeystore(List<ChildNumber> derivation, Keystore keystore) {
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
|
||||
wallet.setName(device.getModel().toDisplayString());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
}
|
||||
|
||||
private void sign() {
|
||||
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
|
||||
signPSBTService.setOnSucceeded(workerStateEvent -> {
|
||||
PSBT signedPsbt = signPSBTService.getValue();
|
||||
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||
Service<PSBT> signService = cardApi.getSignService(wallet, psbt, messageProperty);
|
||||
handleCardOperation(signService, signButton, "Signing", true, event -> {
|
||||
EventManager.get().post(new PSBTSignedEvent(psbt, signService.getValue()));
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Signing Error: " + e.getMessage(), e);
|
||||
setError("Signing Error", e.getMessage());
|
||||
signButton.setDisable(false);
|
||||
}
|
||||
} else {
|
||||
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
|
||||
signPSBTService.setOnSucceeded(workerStateEvent -> {
|
||||
PSBT signedPsbt = signPSBTService.getValue();
|
||||
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
|
||||
});
|
||||
signPSBTService.setOnFailed(workerStateEvent -> {
|
||||
setError("Signing Error", signPSBTService.getException().getMessage());
|
||||
log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException());
|
||||
signButton.setDisable(false);
|
||||
});
|
||||
setDescription("Signing...");
|
||||
showHideLink.setVisible(false);
|
||||
signPSBTService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, boolean pinRequired, EventHandler<WorkerStateEvent> successHandler) {
|
||||
if(pinRequired && pin.get().length() < 6) {
|
||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||
setContent(getCardPinEntry(operationButton));
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
operationButton.setDisable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
service.setOnSucceeded(successHandler);
|
||||
service.setOnFailed(event -> {
|
||||
Throwable rootCause = Throwables.getRootCause(event.getSource().getException());
|
||||
if(rootCause instanceof CardAuthorizationException) {
|
||||
setError(rootCause.getMessage(), null);
|
||||
setContent(getCardPinEntry(operationButton));
|
||||
} else {
|
||||
log.error(operationDescription + " Error: " + rootCause.getMessage(), event.getSource().getException());
|
||||
setError(operationDescription + " Error", rootCause.getMessage());
|
||||
}
|
||||
operationButton.setDisable(false);
|
||||
});
|
||||
signPSBTService.setOnFailed(workerStateEvent -> {
|
||||
setError("Signing Error", signPSBTService.getException().getMessage());
|
||||
log.error("Signing Error: " + signPSBTService.getException().getMessage(), signPSBTService.getException());
|
||||
signButton.setDisable(false);
|
||||
});
|
||||
setDescription("Signing...");
|
||||
showHideLink.setVisible(false);
|
||||
signPSBTService.start();
|
||||
service.start();
|
||||
}
|
||||
|
||||
private void displayAddress() {
|
||||
@@ -647,17 +834,32 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void signMessage() {
|
||||
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
|
||||
signMessageService.setOnSucceeded(successEvent -> {
|
||||
String signature = signMessageService.getValue();
|
||||
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||
});
|
||||
signMessageService.setOnFailed(failedEvent -> {
|
||||
setError("Could not sign message", signMessageService.getException().getMessage());
|
||||
signMessageButton.setDisable(false);
|
||||
});
|
||||
setDescription("Signing message...");
|
||||
signMessageService.start();
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty);
|
||||
handleCardOperation(signMessageService, signMessageButton, "Signing", true, event -> {
|
||||
String signature = signMessageService.getValue();
|
||||
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Signing Error: " + e.getMessage(), e);
|
||||
setError("Signing Error", e.getMessage());
|
||||
signButton.setDisable(false);
|
||||
}
|
||||
} else {
|
||||
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
|
||||
signMessageService.setOnSucceeded(successEvent -> {
|
||||
String signature = signMessageService.getValue();
|
||||
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||
});
|
||||
signMessageService.setOnFailed(failedEvent -> {
|
||||
setError("Could not sign message", signMessageService.getException().getMessage());
|
||||
signMessageButton.setDisable(false);
|
||||
});
|
||||
setDescription("Signing message...");
|
||||
signMessageService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void discoverKeystores() {
|
||||
@@ -717,6 +919,55 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
getXpubsService.start();
|
||||
}
|
||||
|
||||
private void getPrivateKey(Integer slot) {
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||
Service<ECKey> privateKeyService = cardApi.getPrivateKeyService(slot, messageProperty);
|
||||
handleCardOperation(privateKeyService, getPrivateKeyButton, "Private Key", true, event -> {
|
||||
EventManager.get().post(new DeviceGetPrivateKeyEvent(privateKeyService.getValue(), cardApi.getDefaultScriptType()));
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Private Key Error: " + e.getMessage(), e);
|
||||
setError("Private Key Error", e.getMessage());
|
||||
getPrivateKeyButton.setDisable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void getAddress() {
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||
if(!cardApi.isInitialized()) {
|
||||
if(pin.get().length() < 6) {
|
||||
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
|
||||
setContent(getCardPinEntry(getAddressButton));
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
getAddressButton.setDisable(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setDescription("Card not initialized");
|
||||
setContent(getCardInitializationPanel(cardApi, getAddressButton, DeviceOperation.GET_ADDRESS));
|
||||
showHideLink.setVisible(false);
|
||||
setExpanded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
Service<Address> addressService = cardApi.getAddressService(messageProperty);
|
||||
handleCardOperation(addressService, getAddressButton, "Address", false, event -> {
|
||||
EventManager.get().post(new DeviceAddressEvent(addressService.getValue()));
|
||||
});
|
||||
} catch(Exception e) {
|
||||
log.error("Address Error: " + e.getMessage(), e);
|
||||
setError("Address Error", e.getMessage());
|
||||
getAddressButton.setDisable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showOperationButton() {
|
||||
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||
if(defaultDevice) {
|
||||
@@ -724,7 +975,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
importButton.setVisible(true);
|
||||
showHideLink.setText("Show derivation...");
|
||||
showHideLink.setVisible(true);
|
||||
showHideLink.setVisible(!device.isCard());
|
||||
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
|
||||
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
|
||||
@@ -743,6 +994,16 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
discoverKeystoresButton.setDefaultButton(defaultDevice);
|
||||
discoverKeystoresButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.GET_PRIVATE_KEY)) {
|
||||
if(defaultDevice) {
|
||||
getPrivateKeyButton.getStyleClass().add("default-button");
|
||||
}
|
||||
getPrivateKeyButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.GET_ADDRESS)) {
|
||||
getAddressButton.setDefaultButton(defaultDevice);
|
||||
getAddressButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -750,7 +1011,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||
derivationField.setDisable(keyDerivation != null);
|
||||
derivationField.setDisable(device.isCard() || keyDerivation != null);
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
@@ -785,11 +1046,92 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private Node getCardInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
|
||||
VBox initTypeBox = new VBox(5);
|
||||
RadioButton automatic = new RadioButton("Automatic (Recommended)");
|
||||
RadioButton advanced = new RadioButton("Advanced");
|
||||
TextField entropy = new TextField();
|
||||
entropy.setPromptText("Enter input for user entropy");
|
||||
entropy.setDisable(true);
|
||||
|
||||
ToggleGroup toggleGroup = new ToggleGroup();
|
||||
automatic.setToggleGroup(toggleGroup);
|
||||
advanced.setToggleGroup(toggleGroup);
|
||||
automatic.setSelected(true);
|
||||
toggleGroup.selectedToggleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
entropy.setDisable(newValue == automatic);
|
||||
});
|
||||
|
||||
initTypeBox.getChildren().addAll(automatic, advanced, entropy);
|
||||
|
||||
Button initializeButton = new Button("Initialize");
|
||||
initializeButton.setDefaultButton(true);
|
||||
initializeButton.setOnAction(event -> {
|
||||
initializeButton.setDisable(true);
|
||||
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
|
||||
Service<Void> cardInitializationService = cardApi.getInitializationService(chainCode, messageProperty);
|
||||
cardInitializationService.setOnSucceeded(successEvent -> {
|
||||
if(deviceOperation == DeviceOperation.IMPORT) {
|
||||
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
|
||||
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
|
||||
AppServices.showSuccessDialog("Card Reinitialized", "The card was successfully reinitialized.\n\nYou can now retrieve the new deposit address.");
|
||||
}
|
||||
operationButton.setDisable(false);
|
||||
setDefaultStatus();
|
||||
setExpanded(false);
|
||||
});
|
||||
cardInitializationService.setOnFailed(failEvent -> {
|
||||
Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
|
||||
if(rootCause instanceof CardAuthorizationException) {
|
||||
setError(rootCause.getMessage(), null);
|
||||
setContent(getCardPinEntry(operationButton));
|
||||
operationButton.setDisable(false);
|
||||
} else {
|
||||
log.error("Error initializing card", failEvent.getSource().getException());
|
||||
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
|
||||
initializeButton.setDisable(false);
|
||||
}
|
||||
});
|
||||
cardInitializationService.start();
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox(20);
|
||||
contentBox.getChildren().addAll(initTypeBox, initializeButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
HBox.setHgrow(initTypeBox, Priority.ALWAYS);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private Node getCardPinEntry(ButtonBase operationButton) {
|
||||
VBox vBox = new VBox();
|
||||
|
||||
CustomPasswordField pinField = new ViewPasswordField();
|
||||
pinField.setPromptText("PIN Code");
|
||||
if(operationButton instanceof Button defaultButton) {
|
||||
defaultButton.setDefaultButton(true);
|
||||
}
|
||||
pin.bind(pinField.textProperty());
|
||||
HBox.setHgrow(pinField, Priority.ALWAYS);
|
||||
Platform.runLater(pinField::requestFocus);
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().add(pinField);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(50);
|
||||
|
||||
vBox.getChildren().add(contentBox);
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
public Device getDevice() {
|
||||
return device;
|
||||
}
|
||||
|
||||
public enum DeviceOperation {
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES;
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, GET_PRIVATE_KEY, GET_ADDRESS;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,24 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.PSBTSignedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DeviceSignDialog extends DeviceDialog<PSBT> {
|
||||
private static final Logger log = LoggerFactory.getLogger(DeviceSignDialog.class);
|
||||
|
||||
private final Wallet wallet;
|
||||
private final PSBT psbt;
|
||||
|
||||
public DeviceSignDialog(List<String> operationFingerprints, PSBT psbt) {
|
||||
public DeviceSignDialog(Wallet wallet, List<String> operationFingerprints, PSBT psbt) {
|
||||
super(operationFingerprints);
|
||||
this.wallet = wallet;
|
||||
this.psbt = psbt;
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
@@ -23,7 +30,7 @@ public class DeviceSignDialog extends DeviceDialog<PSBT> {
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(psbt, device, defaultDevice);
|
||||
return new DevicePane(wallet, psbt, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.DeviceGetPrivateKeyEvent;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.DevicePrivateKey> {
|
||||
public DeviceUnsealDialog(List<String> operationFingerprints) {
|
||||
super(operationFingerprints);
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
|
||||
return new DevicePane(DevicePane.DeviceOperation.GET_PRIVATE_KEY, device, defaultDevice);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void deviceGetPrivateKey(DeviceGetPrivateKeyEvent event) {
|
||||
setResult(new DevicePrivateKey(event.getPrivateKey(), event.getScriptType()));
|
||||
}
|
||||
|
||||
public record DevicePrivateKey(ECKey privateKey, ScriptType scriptType) {}
|
||||
}
|
||||
@@ -9,9 +9,11 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
@@ -30,7 +32,7 @@ import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
public class EntryCell extends TreeTableCell<Entry, Entry> implements ConfirmationsListener {
|
||||
private static final Logger log = LoggerFactory.getLogger(EntryCell.class);
|
||||
|
||||
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
@@ -38,6 +40,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
private static EntryCell lastCell;
|
||||
|
||||
private IntegerProperty confirmationsProperty;
|
||||
|
||||
public EntryCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
@@ -96,11 +100,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
actionBox.getChildren().add(viewTransactionButton);
|
||||
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
if(blockTransaction.getHeight() <= 0 && blockTransaction.getTransaction().isReplaceByFee() &&
|
||||
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
Button increaseFeeButton = new Button("");
|
||||
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
increaseFeeButton.setOnAction(event -> {
|
||||
increaseFee(transactionEntry);
|
||||
increaseFee(transactionEntry, false);
|
||||
});
|
||||
actionBox.getChildren().add(increaseFeeButton);
|
||||
}
|
||||
@@ -119,7 +124,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||
Address address = nodeEntry.getAddress();
|
||||
setText(address.toString());
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(nodeEntry.getNode().toString());
|
||||
@@ -188,7 +193,22 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
}
|
||||
|
||||
private static void increaseFee(TransactionEntry transactionEntry) {
|
||||
@Override
|
||||
public IntegerProperty getConfirmationsProperty() {
|
||||
if(confirmationsProperty == null) {
|
||||
confirmationsProperty = new SimpleIntegerProperty();
|
||||
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
|
||||
getStyleClass().remove("confirming");
|
||||
confirmationsProperty.unbind();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return confirmationsProperty;
|
||||
}
|
||||
|
||||
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
||||
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
||||
@@ -200,6 +220,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if(utxos.isEmpty()) {
|
||||
log.error("No UTXOs to replace");
|
||||
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction - no replaceable UTXOs were found.");
|
||||
return;
|
||||
}
|
||||
|
||||
List<TransactionOutput> ourOutputs = transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
@@ -222,7 +248,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
//Remove any UTXOs created by the transaction that is to be replaced
|
||||
walletUtxos.removeIf(utxo -> ourOutputs.stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex()));
|
||||
Collections.shuffle(walletUtxos);
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty()) {
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty() && !cancelTransaction) {
|
||||
//If there is insufficient change output, include another random UTXO so the fee can be increased
|
||||
BlockTransactionHashIndex utxo = walletUtxos.remove(0);
|
||||
utxos.add(utxo);
|
||||
@@ -279,6 +305,15 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
return;
|
||||
}
|
||||
|
||||
if(cancelTransaction) {
|
||||
Payment existing = payments.get(0);
|
||||
Address address = transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress();
|
||||
Payment payment = new Payment(address, existing.getLabel(), existing.getAmount(), true);
|
||||
payments.clear();
|
||||
payments.add(payment);
|
||||
opReturns.clear();
|
||||
}
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, blockTransaction.getFee(), true)));
|
||||
}
|
||||
@@ -318,7 +353,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
private static boolean canSignMessage(WalletNode walletNode) {
|
||||
Wallet wallet = walletNode.getWallet();
|
||||
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR &&
|
||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB) &&
|
||||
(wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB || wallet.getKeystores().get(0).getWalletModel().isCard()) &&
|
||||
(!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
@@ -410,6 +445,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
return increaseFeeGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getCancelTransactionRBFGlyph() {
|
||||
Glyph cancelTxGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.BAN);
|
||||
cancelTxGlyph.setFontSize(12);
|
||||
return cancelTxGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getIncreaseFeeCPFPGlyph() {
|
||||
Glyph cpfpGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SIGN_OUT_ALT);
|
||||
cpfpGlyph.setFontSize(12);
|
||||
@@ -463,17 +504,28 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
});
|
||||
getItems().add(viewTransaction);
|
||||
|
||||
if(blockTransaction.getTransaction().isReplaceByFee() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
if(blockTransaction.getTransaction().isReplaceByFee() && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
increaseFee.setOnAction(AE -> {
|
||||
hide();
|
||||
increaseFee(transactionEntry);
|
||||
increaseFee(transactionEntry, false);
|
||||
});
|
||||
|
||||
getItems().add(increaseFee);
|
||||
}
|
||||
|
||||
if(blockTransaction.getTransaction().isReplaceByFee() && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
||||
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
||||
cancelTx.setOnAction(AE -> {
|
||||
hide();
|
||||
increaseFee(transactionEntry, true);
|
||||
});
|
||||
|
||||
getItems().add(cancelTx);
|
||||
}
|
||||
|
||||
if(containsWalletOutputs(transactionEntry)) {
|
||||
MenuItem createCpfp = new MenuItem("Increase Effective Fee (CPFP)");
|
||||
createCpfp.setGraphic(getIncreaseFeeCPFPGlyph());
|
||||
@@ -485,6 +537,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
getItems().add(createCpfp);
|
||||
}
|
||||
|
||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||
openBlockExplorer.setOnAction(AE -> {
|
||||
hide();
|
||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||
});
|
||||
getItems().add(openBlockExplorer);
|
||||
|
||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||
copyTxid.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -506,6 +565,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
||||
});
|
||||
|
||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||
openBlockExplorer.setOnAction(AE -> {
|
||||
hide();
|
||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||
});
|
||||
|
||||
MenuItem copyDate = new MenuItem("Copy Date");
|
||||
copyDate.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -530,12 +595,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(viewTransaction, copyDate, copyTxid, copyHeight);
|
||||
getItems().addAll(viewTransaction, openBlockExplorer, copyDate, copyTxid, copyHeight);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AddressContextMenu extends ContextMenu {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry, boolean addUtxoItems) {
|
||||
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
@@ -558,6 +623,42 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
getItems().add(signVerifyMessage);
|
||||
}
|
||||
|
||||
if(addUtxoItems && nodeEntry != null && !nodeEntry.getNode().getUnspentTransactionOutputs().isEmpty()) {
|
||||
List<BlockTransactionHashIndex> utxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().collect(Collectors.toList());
|
||||
MenuItem spendUtxos = new MenuItem("Spend UTXOs");
|
||||
spendUtxos.setGraphic(getSendGlyph());
|
||||
spendUtxos.setOnAction(AE -> {
|
||||
hide();
|
||||
EventManager.get().post(new SendActionEvent(nodeEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(nodeEntry.getWallet(), utxos)));
|
||||
});
|
||||
getItems().add(spendUtxos);
|
||||
|
||||
List<BlockTransactionHashIndex> unfrozenUtxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().filter(utxo -> utxo.getStatus() != Status.FROZEN).collect(Collectors.toList());
|
||||
if(!unfrozenUtxos.isEmpty()) {
|
||||
MenuItem freezeUtxos = new MenuItem("Freeze UTXOs");
|
||||
freezeUtxos.setGraphic(getFreezeGlyph());
|
||||
freezeUtxos.setOnAction(AE -> {
|
||||
hide();
|
||||
unfrozenUtxos.forEach(utxo -> utxo.setStatus(Status.FROZEN));
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(nodeEntry.getWallet(), unfrozenUtxos));
|
||||
});
|
||||
getItems().add(freezeUtxos);
|
||||
}
|
||||
|
||||
List<BlockTransactionHashIndex> frozenUtxos = nodeEntry.getNode().getUnspentTransactionOutputs().stream().filter(utxo -> utxo.getStatus() == Status.FROZEN).collect(Collectors.toList());
|
||||
if(!frozenUtxos.isEmpty()) {
|
||||
MenuItem unfreezeUtxos = new MenuItem("Unfreeze UTXOs");
|
||||
unfreezeUtxos.setGraphic(getUnfreezeGlyph());
|
||||
unfreezeUtxos.setOnAction(AE -> {
|
||||
hide();
|
||||
frozenUtxos.forEach(utxo -> utxo.setStatus(null));
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(nodeEntry.getWallet(), frozenUtxos));
|
||||
});
|
||||
getItems().add(unfreezeUtxos);
|
||||
}
|
||||
}
|
||||
|
||||
MenuItem copyAddress = new MenuItem("Copy Address");
|
||||
copyAddress.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -566,14 +667,6 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
MenuItem copyHex = new MenuItem("Copy Script Output Bytes");
|
||||
copyHex.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(Utils.bytesToHex(address.getOutputScriptData()));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
MenuItem copyOutputDescriptor = new MenuItem("Copy Output Descriptor");
|
||||
copyOutputDescriptor.setOnAction(AE -> {
|
||||
hide();
|
||||
@@ -582,7 +675,19 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(copyAddress, copyHex, copyOutputDescriptor);
|
||||
getItems().addAll(copyAddress, copyOutputDescriptor);
|
||||
|
||||
if(nodeEntry != null) {
|
||||
MenuItem copyHex = new MenuItem("Copy Script Output Bytes");
|
||||
copyHex.setOnAction(AE -> {
|
||||
hide();
|
||||
Script outputScript = nodeEntry.getWallet().getOutputScript(nodeEntry.getNode());
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(Utils.bytesToHex(outputScript.getProgram()));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyHex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,13 +758,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
if(entry instanceof TransactionEntry) {
|
||||
cell.getStyleClass().add("transaction-row");
|
||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||
if(transactionEntry.isConfirming()) {
|
||||
cell.getStyleClass().add("confirming");
|
||||
transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(!transactionEntry.isConfirming()) {
|
||||
cell.getStyleClass().remove("confirming");
|
||||
}
|
||||
});
|
||||
|
||||
if(cell instanceof ConfirmationsListener confirmationsListener) {
|
||||
if(transactionEntry.isConfirming()) {
|
||||
cell.getStyleClass().add("confirming");
|
||||
confirmationsListener.getConfirmationsProperty().bind(transactionEntry.confirmationsProperty());
|
||||
} else {
|
||||
confirmationsListener.getConfirmationsProperty().unbind();
|
||||
}
|
||||
}
|
||||
} else if(entry instanceof NodeEntry) {
|
||||
cell.getStyleClass().add("node-row");
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
@@ -10,14 +12,9 @@ import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Currency;
|
||||
import java.util.Locale;
|
||||
|
||||
public class FiatLabel extends CopyableLabel {
|
||||
private static final DecimalFormat CURRENCY_FORMAT = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final DoubleProperty btcRateProperty = new SimpleDoubleProperty(0.0);
|
||||
private final ObjectProperty<Currency> currencyProperty = new SimpleObjectProperty<>(null);
|
||||
@@ -30,9 +27,9 @@ public class FiatLabel extends CopyableLabel {
|
||||
|
||||
public FiatLabel(String text) {
|
||||
super(text);
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue));
|
||||
btcRateProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue()));
|
||||
currencyProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue()));
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat()));
|
||||
btcRateProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue(), Config.get().getUnitFormat()));
|
||||
currencyProperty().addListener((observable, oldValue, newValue) -> setValueAsText(getValue(), Config.get().getUnitFormat()));
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new FiatContextMenu();
|
||||
}
|
||||
@@ -83,14 +80,22 @@ public class FiatLabel extends CopyableLabel {
|
||||
setCurrency(currency);
|
||||
}
|
||||
|
||||
private void setValueAsText(long balance) {
|
||||
public void refresh() {
|
||||
refresh(Config.get().getUnitFormat());
|
||||
}
|
||||
|
||||
public void refresh(UnitFormat unitFormat) {
|
||||
setValueAsText(getValue(), unitFormat);
|
||||
}
|
||||
|
||||
private void setValueAsText(long balance, UnitFormat unitFormat) {
|
||||
if(getCurrency() != null && getBtcRate() > 0.0) {
|
||||
BigDecimal satsBalance = BigDecimal.valueOf(balance);
|
||||
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
|
||||
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(getBtcRate()));
|
||||
|
||||
String label = getCurrency().getSymbol() + " " + CURRENCY_FORMAT.format(fiatBalance.doubleValue());
|
||||
tooltip.setText("1 BTC = " + getCurrency().getSymbol() + " " + CURRENCY_FORMAT.format(getBtcRate()));
|
||||
String label = getCurrency().getSymbol() + " " + unitFormat.formatCurrencyValue(fiatBalance.doubleValue());
|
||||
tooltip.setText("1 BTC = " + getCurrency().getSymbol() + " " + unitFormat.formatCurrencyValue(getBtcRate()));
|
||||
|
||||
setText(label);
|
||||
setTooltip(tooltip);
|
||||
|
||||
@@ -162,8 +162,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
setError("Import Error", e.getMessage());
|
||||
}
|
||||
} else if(result.outputDescriptor != null) {
|
||||
wallets = List.of(result.outputDescriptor.toKeystoreWallet(null));
|
||||
try {
|
||||
wallets = List.of(result.outputDescriptor.toWallet());
|
||||
importFile(importer.getName(), null, null);
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing QR", e);
|
||||
@@ -193,6 +193,10 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
protected List<Wallet> getScannedWallets() {
|
||||
return wallets;
|
||||
}
|
||||
|
||||
protected Keystore getScannedKeystore(ScriptType scriptType) throws ImportException {
|
||||
if(wallets != null) {
|
||||
for(Wallet wallet : wallets) {
|
||||
@@ -215,7 +219,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
|
||||
private Node getPasswordEntry(File file) {
|
||||
CustomPasswordField passwordField = new ViewPasswordField();
|
||||
passwordField.setPromptText("Wallet password");
|
||||
passwordField.setPromptText("Password");
|
||||
password.bind(passwordField.textProperty());
|
||||
HBox.setHgrow(passwordField, Priority.ALWAYS);
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
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.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class FileKeystoreExportPane extends TitledDescriptionPane {
|
||||
private final Keystore keystore;
|
||||
private final KeystoreFileExport exporter;
|
||||
private final boolean scannable;
|
||||
private final boolean file;
|
||||
|
||||
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
|
||||
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||
this.keystore = keystore;
|
||||
this.exporter = exporter;
|
||||
this.scannable = exporter.isKeystoreExportScannable();
|
||||
this.file = exporter.isKeystoreExportFile();
|
||||
|
||||
buttonBox.getChildren().clear();
|
||||
buttonBox.getChildren().add(createButton());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
if(scannable && file) {
|
||||
ToggleButton showButton = new ToggleButton("Show...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
showButton.setGraphic(cameraGlyph);
|
||||
showButton.setOnAction(event -> {
|
||||
showButton.setSelected(false);
|
||||
exportQR();
|
||||
});
|
||||
|
||||
ToggleButton fileButton = new ToggleButton("Export File...");
|
||||
fileButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
fileButton.setOnAction(event -> {
|
||||
fileButton.setSelected(false);
|
||||
exportFile();
|
||||
});
|
||||
|
||||
SegmentedButton segmentedButton = new SegmentedButton();
|
||||
segmentedButton.getButtons().addAll(showButton, fileButton);
|
||||
return segmentedButton;
|
||||
} else if(scannable) {
|
||||
Button showButton = new Button("Show...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
showButton.setGraphic(cameraGlyph);
|
||||
showButton.setOnAction(event -> {
|
||||
exportQR();
|
||||
});
|
||||
return showButton;
|
||||
} else {
|
||||
Button exportButton = new Button("Export File...");
|
||||
exportButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
exportButton.setOnAction(event -> {
|
||||
exportFile();
|
||||
});
|
||||
return exportButton;
|
||||
}
|
||||
}
|
||||
|
||||
private void exportQR() {
|
||||
exportKeystore(null, keystore);
|
||||
}
|
||||
|
||||
private void exportFile() {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
|
||||
String extension = exporter.getExportFileExtension(keystore);
|
||||
String fileName = keystore.getLabel();
|
||||
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
exportKeystore(file, keystore);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportKeystore(File file, Keystore exportKeystore) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
exporter.exportKeystore(exportKeystore, baos);
|
||||
|
||||
if(exporter.requiresSignature()) {
|
||||
String message = baos.toString(StandardCharsets.UTF_8);
|
||||
|
||||
if(keystore.getSource() == KeystoreSource.HW_USB || keystore.getWalletModel().isCard()) {
|
||||
TextAreaDialog dialog = new TextAreaDialog(message, false);
|
||||
dialog.setTitle("Sign " + exporter.getName() + " Export");
|
||||
dialog.getDialogPane().setHeaderText("The following text needs to be signed by the device.\nClick OK to continue.");
|
||||
dialog.showAndWait();
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setScriptType(ScriptType.P2PKH);
|
||||
wallet.getKeystores().add(keystore);
|
||||
List<String> operationFingerprints = List.of(keystore.getKeyDerivation().getMasterFingerprint());
|
||||
|
||||
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(operationFingerprints, wallet, message, keystore.getKeyDerivation());
|
||||
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
||||
if(optSignature.isPresent()) {
|
||||
exporter.addSignature(keystore, optSignature.get(), baos);
|
||||
}
|
||||
} else if(keystore.getSource() == KeystoreSource.SW_SEED) {
|
||||
String signature = keystore.getExtendedPrivateKey().getKey().signMessage(message, ScriptType.P2PKH);
|
||||
exporter.addSignature(keystore, signature, baos);
|
||||
} else {
|
||||
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Cannot sign export",
|
||||
"Signing the " + exporter.getName() + " export with " + keystore.getWalletModel().toDisplayString() + " is not supported." +
|
||||
"Proceed without signing?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.NO) {
|
||||
throw new RuntimeException("Export aborted due to lack of device message signing support.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(file != null) {
|
||||
try(OutputStream outputStream = new FileOutputStream(file)) {
|
||||
outputStream.write(baos.toByteArray());
|
||||
EventManager.get().post(new KeystoreExportEvent(exportKeystore));
|
||||
}
|
||||
} else {
|
||||
QRDisplayDialog qrDisplayDialog;
|
||||
if(exporter instanceof Bip129) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), baos.toByteArray(), false);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
qrDisplayDialog.showAndWait();
|
||||
}
|
||||
} catch(Exception e) {
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Export Error", errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,17 +34,4 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
}
|
||||
|
||||
private static int getAccount(Wallet wallet, KeyDerivation requiredDerivation) {
|
||||
if(wallet == null || requiredDerivation == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int account = wallet.getScriptType().getAccount(requiredDerivation.getDerivationPath());
|
||||
if(account < 0) {
|
||||
account = 0;
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,12 +28,14 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
private final Wallet wallet;
|
||||
private final WalletExport exporter;
|
||||
private final boolean scannable;
|
||||
private final boolean file;
|
||||
|
||||
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
|
||||
super(exporter.getName(), "Wallet file export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||
this.wallet = wallet;
|
||||
this.exporter = exporter;
|
||||
this.scannable = exporter.isWalletExportScannable();
|
||||
this.file = exporter.isWalletExportFile();
|
||||
|
||||
buttonBox.getChildren().clear();
|
||||
buttonBox.getChildren().add(createButton());
|
||||
@@ -41,7 +43,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
if(scannable) {
|
||||
if(scannable && file) {
|
||||
ToggleButton showButton = new ToggleButton("Show...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
@@ -61,6 +63,15 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
SegmentedButton segmentedButton = new SegmentedButton();
|
||||
segmentedButton.getButtons().addAll(showButton, fileButton);
|
||||
return segmentedButton;
|
||||
} else if(scannable) {
|
||||
Button showButton = new Button("Show...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
showButton.setGraphic(cameraGlyph);
|
||||
showButton.setOnAction(event -> {
|
||||
exportQR();
|
||||
});
|
||||
return showButton;
|
||||
} else {
|
||||
Button exportButton = new Button("Export File...");
|
||||
exportButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -81,9 +92,11 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
|
||||
String extension = exporter.getExportFileExtension(wallet);
|
||||
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase(Locale.ROOT).replace(" ", "");
|
||||
if(exporter instanceof Sparrow) {
|
||||
fileName = wallet.getMasterName();
|
||||
String walletModel = exporter.getWalletModel().toDisplayString().toLowerCase(Locale.ROOT).replace(" ", "");
|
||||
String postfix = walletModel.equals(extension) ? "" : "-" + walletModel;
|
||||
String fileName = wallet.getFullName() + postfix;
|
||||
if(exporter.exportsAllWallets()) {
|
||||
fileName = wallet.getMasterName() + postfix;
|
||||
}
|
||||
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
|
||||
|
||||
@@ -137,7 +150,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
QRDisplayDialog qrDisplayDialog;
|
||||
if(exporter instanceof CoboVaultMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig) {
|
||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig || exporter instanceof Bip129) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||
|
||||
@@ -18,7 +18,13 @@ public class FileWalletImportPane extends FileImportPane {
|
||||
|
||||
@Override
|
||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||
Wallet wallet = importer.importWallet(inputStream, password);
|
||||
Wallet wallet;
|
||||
if(getScannedWallets() != null && !getScannedWallets().isEmpty()) {
|
||||
wallet = getScannedWallets().iterator().next();
|
||||
} else {
|
||||
wallet = importer.importWallet(inputStream, password);
|
||||
}
|
||||
|
||||
if(wallet.getName() == null) {
|
||||
wallet.setName(fileName);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class KeystoreExportDialog extends Dialog<Keystore> {
|
||||
public KeystoreExportDialog(Keystore keystore) {
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
});
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
dialogPane.setContent(stackPane);
|
||||
|
||||
AnchorPane anchorPane = new AnchorPane();
|
||||
stackPane.getChildren().add(anchorPane);
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setPrefHeight(200);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
anchorPane.getChildren().add(scrollPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
AnchorPane.setLeftAnchor(scrollPane, 0.0);
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
List<KeystoreFileExport> exporters = List.of(new Bip129());
|
||||
|
||||
Accordion exportAccordion = new Accordion();
|
||||
for(KeystoreFileExport exporter : exporters) {
|
||||
if(!exporter.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileKeystoreExportPane exportPane = new FileKeystoreExportPane(keystore, exporter);
|
||||
exportAccordion.getPanes().add(exportPane);
|
||||
}
|
||||
}
|
||||
|
||||
exportAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||
scrollPane.setContent(exportAccordion);
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(280);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? keystore : null);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void keystoreExported(KeystoreExportEvent event) {
|
||||
setResult(event.getKeystore());
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
public class KeystorePassphraseDialog extends Dialog<String> {
|
||||
private final CustomPasswordField passphrase;
|
||||
private final ObjectProperty<byte[]> masterFingerprint = new SimpleObjectProperty<>();
|
||||
|
||||
public KeystorePassphraseDialog(Keystore keystore) {
|
||||
this(null, keystore);
|
||||
@@ -45,10 +48,38 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||
content.setPrefHeight(50);
|
||||
content.getChildren().add(passphrase);
|
||||
|
||||
passphrase.textProperty().addListener((observable, oldValue, passphrase) -> {
|
||||
masterFingerprint.set(getMasterFingerprint(keystore, passphrase));
|
||||
});
|
||||
|
||||
HBox fingerprintBox = new HBox(10);
|
||||
fingerprintBox.setAlignment(Pos.CENTER_LEFT);
|
||||
Label fingerprintLabel = new Label("Master fingerprint:");
|
||||
TextField fingerprintHex = new TextField();
|
||||
fingerprintHex.setDisable(true);
|
||||
fingerprintHex.setMaxWidth(80);
|
||||
fingerprintHex.getStyleClass().addAll("fixed-width");
|
||||
fingerprintHex.setStyle("-fx-opacity: 0.6");
|
||||
masterFingerprint.addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null) {
|
||||
fingerprintHex.setText(Utils.bytesToHex(newValue));
|
||||
}
|
||||
});
|
||||
LifeHashIcon lifeHashIcon = new LifeHashIcon();
|
||||
lifeHashIcon.dataProperty().bind(masterFingerprint);
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setHelpText("All passphrases create valid wallets." +
|
||||
"\nThe master fingerprint identifies the keystore and changes as the passphrase changes." +
|
||||
"\n" + (confirm ? "Take a moment to identify it before proceeding." : "Make sure you recognise it before proceeding."));
|
||||
fingerprintBox.getChildren().addAll(fingerprintLabel, fingerprintHex, lifeHashIcon, helpLabel);
|
||||
content.getChildren().add(fingerprintBox);
|
||||
|
||||
masterFingerprint.set(getMasterFingerprint(keystore, ""));
|
||||
|
||||
Glyph warnGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
warnGlyph.getStyleClass().add("warn-icon");
|
||||
warnGlyph.setFontSize(12);
|
||||
Label warnLabel = new Label("A BIP39 passphrase is not a wallet password!", warnGlyph);
|
||||
Label warnLabel = new Label((confirm ? "Note" : "Check") + " the master fingerprint before proceeding!", warnGlyph);
|
||||
warnLabel.setGraphicTextGap(5);
|
||||
content.getChildren().add(warnLabel);
|
||||
|
||||
@@ -57,4 +88,14 @@ public class KeystorePassphraseDialog extends Dialog<String> {
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? passphrase.getText() : null);
|
||||
}
|
||||
|
||||
private byte[] getMasterFingerprint(Keystore keystore, String passphrase) {
|
||||
try {
|
||||
Keystore copyKeystore = keystore.copy();
|
||||
copyKeystore.getSeed().setPassphrase(passphrase);
|
||||
return copyKeystore.getExtendedMasterPrivateKey().getKey().getFingerprint();
|
||||
} catch(MnemonicException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.event.Event;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTreeTableCell;
|
||||
@@ -13,9 +17,11 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
class LabelCell extends TextFieldTreeTableCell<Entry, String> implements ConfirmationsListener {
|
||||
private static final Logger log = LoggerFactory.getLogger(LabelCell.class);
|
||||
|
||||
private IntegerProperty confirmationsProperty;
|
||||
|
||||
public LabelCell() {
|
||||
super(new DefaultStringConverter());
|
||||
getStyleClass().add("label-cell");
|
||||
@@ -60,6 +66,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
}
|
||||
|
||||
super.commitEdit(label);
|
||||
Platform.runLater(() -> getTreeTableView().requestFocus());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,6 +88,21 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntegerProperty getConfirmationsProperty() {
|
||||
if(confirmationsProperty == null) {
|
||||
confirmationsProperty = new SimpleIntegerProperty();
|
||||
confirmationsProperty.addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue.intValue() >= BlockTransactionHash.BLOCKS_TO_CONFIRM) {
|
||||
getStyleClass().remove("confirming");
|
||||
confirmationsProperty.unbind();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return confirmationsProperty;
|
||||
}
|
||||
|
||||
private static class LabelContextMenu extends ContextMenu {
|
||||
public LabelContextMenu(Entry entry, String label) {
|
||||
MenuItem copyLabel = new MenuItem("Copy Label");
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.sparrow.io.ImageUtils;
|
||||
import com.sparrowwallet.toucan.LifeHash;
|
||||
import com.sparrowwallet.toucan.LifeHashVersion;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.ImagePattern;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class LifeHashIcon extends Group {
|
||||
private static final Logger log = LoggerFactory.getLogger(LifeHashIcon.class);
|
||||
|
||||
private static final int SIZE = 24;
|
||||
|
||||
private final ObjectProperty<byte[]> dataProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
public LifeHashIcon() {
|
||||
super();
|
||||
|
||||
dataProperty.addListener((observable, oldValue, data) -> {
|
||||
if(data == null) {
|
||||
getChildren().clear();
|
||||
} else if(oldValue == null || !Arrays.equals(oldValue, data)) {
|
||||
LifeHash.Image lifeHashImage = LifeHash.makeFromData(data, LifeHashVersion.VERSION2, 1, false);
|
||||
BufferedImage bufferedImage = LifeHash.getBufferedImage(lifeHashImage);
|
||||
BufferedImage resizedImage = ImageUtils.resizeToImage(bufferedImage, SIZE, SIZE);
|
||||
Image image = SwingFXUtils.toFXImage(resizedImage, null);
|
||||
setImage(image);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setImage(Image image) {
|
||||
getChildren().clear();
|
||||
Rectangle rectangle = new Rectangle(SIZE, SIZE);
|
||||
rectangle.setArcWidth(6);
|
||||
rectangle.setArcHeight(6);
|
||||
rectangle.setFill(new ImagePattern(image));
|
||||
rectangle.setStroke(Color.rgb(65, 72, 77));
|
||||
rectangle.setStrokeWidth(1.0);
|
||||
getChildren().add(rectangle);
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
return dataProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<byte[]> dataProperty() {
|
||||
return dataProperty;
|
||||
}
|
||||
|
||||
public void setData(byte[] data) {
|
||||
this.dataProperty.set(data);
|
||||
}
|
||||
|
||||
public void setHex(String hex) {
|
||||
setData(hex == null ? null : Utils.hexToBytes(hex));
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,7 @@ import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
|
||||
@@ -269,8 +266,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required");
|
||||
}
|
||||
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a USB keystore");
|
||||
if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB && !wallet.getKeystores().get(0).getWalletModel().isCard()) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a connected keystore");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +292,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private boolean isValidAddress() {
|
||||
try {
|
||||
Address address = getAddress();
|
||||
return address.getScriptType() != ScriptType.P2TR && address.getScriptType().isAllowed(PolicyType.SINGLE);
|
||||
return address.getScriptType() != ScriptType.P2TR && (address.getScriptType().isAllowed(PolicyType.SINGLE) || address.getScriptType() == ScriptType.P2SH);
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -319,8 +316,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
} else {
|
||||
signUnencryptedKeystore(signingWallet);
|
||||
}
|
||||
} else if(signingWallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
signUsbKeystore(signingWallet);
|
||||
} else if(signingWallet.containsSource(KeystoreSource.HW_USB) || wallet.getKeystores().get(0).getWalletModel().isCard()) {
|
||||
signDeviceKeystore(signingWallet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,10 +336,10 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private void signUsbKeystore(Wallet usbWallet) {
|
||||
List<String> fingerprints = List.of(usbWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
||||
KeyDerivation fullDerivation = usbWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
|
||||
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, usbWallet, message.getText().trim(), fullDerivation);
|
||||
private void signDeviceKeystore(Wallet deviceWallet) {
|
||||
List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
||||
KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
|
||||
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
|
||||
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
||||
if(optSignature.isPresent()) {
|
||||
signature.clear();
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.PdfUtils;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.controlsfx.control.spreadsheet.*;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
private final SpreadsheetView spreadsheetView;
|
||||
|
||||
private final int GRID_COLUMN_COUNT = 16;
|
||||
|
||||
private final BooleanProperty initializedProperty = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty wordsSelectedProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
public MnemonicGridDialog() {
|
||||
DialogPane dialogPane = new MnemonicGridDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
setTitle("Border Wallets Grid");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("grid.css").toExternalForm());
|
||||
dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid. Note the order of selection is important!");
|
||||
javafx.scene.image.Image image = new Image("/image/border-wallets.png");
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
|
||||
String[][] emptyWordGrid = new String[128][GRID_COLUMN_COUNT];
|
||||
Grid grid = getGrid(emptyWordGrid);
|
||||
|
||||
spreadsheetView = new SpreadsheetView(grid);
|
||||
spreadsheetView.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
try {
|
||||
Field f = event.getClass().getDeclaredField(Platform.getCurrent() == Platform.OSX ? "metaDown" : "controlDown");
|
||||
f.setAccessible(true);
|
||||
f.set(event, true);
|
||||
} catch(IllegalAccessException | NoSuchFieldException e) {
|
||||
//ignore
|
||||
}
|
||||
});
|
||||
spreadsheetView.setId("grid");
|
||||
spreadsheetView.setEditable(false);
|
||||
spreadsheetView.setFixingColumnsAllowed(false);
|
||||
spreadsheetView.setFixingRowsAllowed(false);
|
||||
|
||||
spreadsheetView.getSelectionModel().getSelectedCells().addListener(new ListChangeListener<>() {
|
||||
@Override
|
||||
public void onChanged(Change<? extends TablePosition> c) {
|
||||
int numWords = c.getList().size();
|
||||
wordsSelectedProperty.set(numWords == 11 || numWords == 23);
|
||||
}
|
||||
});
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(spreadsheetView);
|
||||
dialogPane.setContent(stackPane);
|
||||
|
||||
stackPane.widthProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null) {
|
||||
for(SpreadsheetColumn column : spreadsheetView.getColumns()) {
|
||||
column.setPrefWidth((newValue.doubleValue() - spreadsheetView.getRowHeaderWidth() - 3) / 17);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
final ButtonType loadCsvButtonType = new javafx.scene.control.ButtonType("Load PDF...", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(loadCsvButtonType);
|
||||
|
||||
final ButtonType generateButtonType = new javafx.scene.control.ButtonType("Generate Grid...", ButtonBar.ButtonData.HELP);
|
||||
dialogPane.getButtonTypes().add(generateButtonType);
|
||||
|
||||
final ButtonType clearButtonType = new javafx.scene.control.ButtonType("Clear Selection", ButtonBar.ButtonData.OTHER);
|
||||
dialogPane.getButtonTypes().add(clearButtonType);
|
||||
|
||||
Button okButton = (Button)dialogPane.lookupButton(ButtonType.OK);
|
||||
okButton.disableProperty().bind(Bindings.not(Bindings.and(initializedProperty, wordsSelectedProperty)));
|
||||
|
||||
setResultConverter((dialogButton) -> {
|
||||
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||
return data == ButtonBar.ButtonData.OK_DONE ? getSelectedWords() : null;
|
||||
});
|
||||
|
||||
dialogPane.setPrefWidth(850);
|
||||
dialogPane.setPrefHeight(500);
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
private Grid getGrid(String[][] wordGrid) {
|
||||
int rowCount = wordGrid.length;
|
||||
int columnCount = wordGrid[0].length;
|
||||
GridBase grid = new GridBase(rowCount, columnCount);
|
||||
ObservableList<ObservableList<SpreadsheetCell>> rows = FXCollections.observableArrayList();
|
||||
grid.getColumnHeaders().setAll("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P");
|
||||
for(int i = 0; i < rowCount; i++) {
|
||||
final ObservableList<SpreadsheetCell> list = FXCollections.observableArrayList();
|
||||
for(int j = 0; j < columnCount; j++) {
|
||||
list.add(createCell(i, j, wordGrid[i][j]));
|
||||
}
|
||||
rows.add(list);
|
||||
grid.getRowHeaders().add(String.format("%03d", i + 1));
|
||||
}
|
||||
grid.setRows(rows);
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
private SpreadsheetCell createCell(int row, int column, String word) {
|
||||
return SpreadsheetCellType.STRING.createCell(row, column, 1, 1, word == null ? "" : word);
|
||||
}
|
||||
|
||||
private List<String> getSelectedWords() {
|
||||
List<String> abbreviations = spreadsheetView.getSelectionModel().getSelectedCells().stream()
|
||||
.map(position -> (String)spreadsheetView.getGrid().getRows().get(position.getRow()).get(position.getColumn()).getItem()).collect(Collectors.toList());
|
||||
|
||||
List<String> words = new ArrayList<>();
|
||||
for(String abbreviation : abbreviations) {
|
||||
for(String word : Bip39MnemonicCode.INSTANCE.getWordList()) {
|
||||
if((abbreviation.length() == 3 && word.equals(abbreviation)) || (abbreviation.length() >= 4 && word.startsWith(abbreviation))) {
|
||||
words.add(word);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(words.size() != abbreviations.size()) {
|
||||
abbreviations.removeIf(abbr -> words.stream().anyMatch(w -> w.startsWith(abbr)));
|
||||
throw new IllegalStateException("Could not find words for abbreviations: " + abbreviations);
|
||||
}
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
public List<String> shuffle(List<String> mnemonic) {
|
||||
String mnemonicString = String.join(" ", mnemonic);
|
||||
List<String> words = new ArrayList<>(Bip39MnemonicCode.INSTANCE.getWordList());
|
||||
|
||||
UltraHighEntropyPrng uhePrng = new UltraHighEntropyPrng();
|
||||
uhePrng.initState();
|
||||
uhePrng.hashString(mnemonicString);
|
||||
for(int i = words.size() - 1; i > 0; i--) {
|
||||
int j = (int)uhePrng.random(i + 1);
|
||||
String tmp = words.get(i);
|
||||
words.set(i, words.get(j));
|
||||
words.set(j, tmp);
|
||||
}
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
private String[][] toGrid(List<String> words) {
|
||||
String[][] grid = new String[words.size()/GRID_COLUMN_COUNT][GRID_COLUMN_COUNT];
|
||||
|
||||
int row = 0;
|
||||
int col = 0;
|
||||
for(String word : words) {
|
||||
String abbr = word.length() < 4 ? word : word.substring(0, 4);
|
||||
grid[row][col] = abbr;
|
||||
col++;
|
||||
if(col >= GRID_COLUMN_COUNT) {
|
||||
col = 0;
|
||||
row++;
|
||||
}
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
private class MnemonicGridDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
Node button;
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
Button loadButton = new Button(buttonType.getText());
|
||||
loadButton.setGraphicTextGap(5);
|
||||
loadButton.setGraphic(getGlyph(FontAwesome5.Glyph.ARROW_UP));
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(loadButton, buttonData);
|
||||
loadButton.setOnAction(event -> {
|
||||
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("PDF", "*.pdf")
|
||||
);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(this.getScene().getWindow(), 800, 450);
|
||||
File file = fileChooser.showOpenDialog(this.getScene().getWindow());
|
||||
if(file != null) {
|
||||
try(BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||
String[][] wordGrid = PdfUtils.getWordGrid(inputStream);
|
||||
spreadsheetView.setGrid(getGrid(wordGrid));
|
||||
initializedProperty.set(true);
|
||||
} catch(Exception e) {
|
||||
AppServices.showErrorDialog("Cannot load PDF", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
button = loadButton;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP) {
|
||||
Button generateButton = new Button(buttonType.getText());
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(generateButton, buttonData);
|
||||
generateButton.setOnAction(event -> {
|
||||
SeedEntryDialog seedEntryDialog = new SeedEntryDialog(12);
|
||||
Optional<List<String>> optWords = seedEntryDialog.showAndWait();
|
||||
if(optWords.isPresent()) {
|
||||
List<String> mnemonicWords = optWords.get();
|
||||
List<String> shuffledWordList = shuffle(mnemonicWords);
|
||||
String[][] wordGrid = toGrid(shuffledWordList);
|
||||
spreadsheetView.setGrid(getGrid(wordGrid));
|
||||
initializedProperty.set(true);
|
||||
|
||||
if(seedEntryDialog.isGenerated()) {
|
||||
PdfUtils.saveWordGrid(wordGrid, mnemonicWords);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
button = generateButton;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.OTHER) {
|
||||
Button clearButton = new Button(buttonType.getText());
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(clearButton, buttonData);
|
||||
clearButton.setOnAction(event -> {
|
||||
spreadsheetView.getSelectionModel().clearSelection();
|
||||
});
|
||||
|
||||
button = clearButton;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
|
||||
public static class UltraHighEntropyPrng {
|
||||
private final int order;
|
||||
private double carry;
|
||||
private int phase;
|
||||
private final double[] intermediates;
|
||||
private int i, j, k; // general purpose locals
|
||||
|
||||
public UltraHighEntropyPrng() {
|
||||
order = 48; // set the 'order' number of ENTROPY-holding 32-bit values
|
||||
carry = 1; // init the 'carry' used by the multiply-with-carry (MWC) algorithm
|
||||
phase = order; // init the 'phase' (max-1) of the intermediate variable pointer
|
||||
intermediates = new double[order]; // declare our intermediate variables array
|
||||
|
||||
for(i = 0; i < order; i++) {
|
||||
// Used to simulate javascript's Math.random
|
||||
Random random = new Random();
|
||||
intermediates[i] = mash(random.nextInt(Integer.MAX_VALUE)); // fill the array with initial mash hash values
|
||||
}
|
||||
}
|
||||
|
||||
public double random(int range) {
|
||||
return Math.floor(range * (rawPrng() + ((long)(rawPrng() * 0x200000L)) * 1.1102230246251565e-16)); // 2^-53
|
||||
}
|
||||
|
||||
private String randomString(int count) {
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
for(i = 0; i < count; i++) {
|
||||
char newChar = (char) (33 + random(94));
|
||||
stringBuilder.append(newChar);
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
private double rawPrng() {
|
||||
if(++phase >= order) {
|
||||
phase = 0;
|
||||
}
|
||||
double t = 1768863 * intermediates[phase] + carry * 2.3283064365386963e-10; // 2^-32
|
||||
long temp = (long)t;
|
||||
return intermediates[phase] = t - (carry = temp);
|
||||
}
|
||||
|
||||
private void hash(String args) {
|
||||
for(i = 0; i < args.length(); i++) {
|
||||
for(j = 0; j < order; j++) {
|
||||
intermediates[j] -= mash(args.charAt(i));
|
||||
if(intermediates[j] < 0) {
|
||||
intermediates[j] = intermediates[j] + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void hashString(String input) {
|
||||
mash(input); // use the string to evolve the 'mash' state
|
||||
|
||||
char[] inputAry = input.toCharArray();
|
||||
for(i = 0; i < inputAry.length; i++) // scan through the characters in our string
|
||||
{
|
||||
k = inputAry[i]; // get the character code at the location
|
||||
for(j = 0; j < order; j++) // "mash" it into the UHEPRNG state
|
||||
{
|
||||
intermediates[j] -= mash(k);
|
||||
if(intermediates[j] < 0) {
|
||||
intermediates[j] += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void initState() {
|
||||
mash(null);
|
||||
for(i = 0; i < order; i++) {
|
||||
intermediates[i] = mash(' '); // fill the array with initial mash hash values
|
||||
}
|
||||
carry = 1; // init our multiply-with-carry carry
|
||||
phase = order; // init our phase
|
||||
}
|
||||
|
||||
double n = Integer.toUnsignedLong(0xefc8249d);
|
||||
|
||||
private double mash(Object data) {
|
||||
if(data != null) {
|
||||
String strData = data.toString();
|
||||
for(int i = 0; i < strData.length(); i++) {
|
||||
n += strData.charAt(i);
|
||||
double h = 0.02519603282416938 * n;
|
||||
n = Integer.toUnsignedLong((int)h);
|
||||
h -= n;
|
||||
h *= n;
|
||||
n = Integer.toUnsignedLong((int)h);
|
||||
h -= n;
|
||||
n += h * 0x100000000L; // 2^32
|
||||
}
|
||||
return ((long)n) * 2.3283064365386963e-10; // 2^-32
|
||||
} else {
|
||||
n = Integer.toUnsignedLong(0xefc8249d);
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
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;
|
||||
@@ -24,7 +23,8 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
showWordList(keystore.getSeed());
|
||||
}
|
||||
|
||||
protected Node getMnemonicWordsEntry(int numWords) {
|
||||
@Override
|
||||
protected Node getMnemonicWordsEntry(int numWords, boolean showPassphrase, boolean editPassphrase) {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
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.WalletModel;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
|
||||
public class MnemonicKeystoreEntryPane extends MnemonicKeystorePane {
|
||||
private final BooleanProperty validProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
private boolean generated;
|
||||
|
||||
public MnemonicKeystoreEntryPane(int numWords) {
|
||||
super(DeterministicSeed.Type.BIP39.getName(), "Enter seed words", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
showHideLink.setVisible(false);
|
||||
buttonBox.getChildren().clear();
|
||||
|
||||
defaultWordSizeProperty.set(numWords);
|
||||
setDescription("Generate new or enter existing");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getMnemonicWordsEntry(numWords, false, true));
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Node> createRightButtons() {
|
||||
Button button = new Button("Next");
|
||||
button.setVisible(false);
|
||||
return List.of(button);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
Bip39MnemonicCode.INSTANCE.check(wordEntriesProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(MnemonicException e) {
|
||||
invalidLabel.setText("Invalid checksum");
|
||||
invalidLabel.setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
validProperty.set(validChecksum);
|
||||
validLabel.setVisible(validChecksum);
|
||||
invalidLabel.setVisible(!validChecksum && !empty);
|
||||
}
|
||||
|
||||
public void generateNew() {
|
||||
int mnemonicSeedLength = wordEntriesProperty.get().size() * 11;
|
||||
int entropyLength = mnemonicSeedLength - (mnemonicSeedLength/33);
|
||||
|
||||
SecureRandom secureRandom;
|
||||
try {
|
||||
secureRandom = SecureRandom.getInstanceStrong();
|
||||
} catch(NoSuchAlgorithmException e) {
|
||||
secureRandom = new SecureRandom();
|
||||
}
|
||||
|
||||
DeterministicSeed deterministicSeed = new DeterministicSeed(secureRandom, entropyLength, "");
|
||||
displayMnemonicCode(deterministicSeed);
|
||||
generated = true;
|
||||
}
|
||||
|
||||
private void displayMnemonicCode(DeterministicSeed deterministicSeed) {
|
||||
setDescription("Write down these words");
|
||||
showHideLink.setVisible(false);
|
||||
|
||||
for (int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
|
||||
wordEntry.getEditor().setText(deterministicSeed.getMnemonicCode().get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return validProperty.get();
|
||||
}
|
||||
|
||||
public BooleanProperty validProperty() {
|
||||
return validProperty;
|
||||
}
|
||||
|
||||
public boolean isGenerated() {
|
||||
return generated;
|
||||
}
|
||||
}
|
||||
@@ -223,7 +223,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
private void confirmBackup() {
|
||||
setDescription("Confirm backup by re-entering words");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size()));
|
||||
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), true, false));
|
||||
setExpanded(true);
|
||||
backButton.setVisible(true);
|
||||
generateButton.setVisible(false);
|
||||
|
||||
@@ -12,6 +12,8 @@ import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -20,8 +22,8 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.textfield.AutoCompletionBinding;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
@@ -44,7 +46,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
protected Label invalidLabel;
|
||||
|
||||
protected SimpleListProperty<String> wordEntriesProperty;
|
||||
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
|
||||
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty("");
|
||||
protected IntegerProperty defaultWordSizeProperty;
|
||||
|
||||
public MnemonicKeystorePane(String title, String description, String content, String imageUrl) {
|
||||
@@ -79,6 +81,12 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
enterMnemonicButton.getItems().add(item);
|
||||
}
|
||||
enterMnemonicButton.getItems().add(new SeparatorMenuItem());
|
||||
MenuItem gridItem = new MenuItem("Border Wallets Grid...");
|
||||
gridItem.setOnAction(event -> {
|
||||
showGrid();
|
||||
});
|
||||
enterMnemonicButton.getItems().add(gridItem);
|
||||
|
||||
MenuItem scanItem = new MenuItem("Scan QR...");
|
||||
scanItem.setOnAction(event -> {
|
||||
scanQR();
|
||||
@@ -87,6 +95,42 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
||||
}
|
||||
|
||||
protected void showGrid() {
|
||||
MnemonicGridDialog mnemonicGridDialog = new MnemonicGridDialog();
|
||||
Optional<List<String>> optWords = mnemonicGridDialog.showAndWait();
|
||||
if(optWords.isPresent()) {
|
||||
List<String> words = optWords.get();
|
||||
setContent(getMnemonicWordsEntry(words.size() + 1, true, true));
|
||||
setExpanded(true);
|
||||
|
||||
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
|
||||
if(i < words.size()) {
|
||||
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();
|
||||
Platform.runLater(() -> wordEntry.getEditor().requestFocus());
|
||||
});
|
||||
service.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void scanQR() {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||
@@ -106,7 +150,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
protected void showWordList(DeterministicSeed seed) {
|
||||
List<String> words = seed.getMnemonicCode();
|
||||
setContent(getMnemonicWordsEntry(words.size()));
|
||||
setContent(getMnemonicWordsEntry(words.size(), true, true));
|
||||
setExpanded(true);
|
||||
|
||||
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||
@@ -119,11 +163,11 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
protected void enterMnemonic(int numWords) {
|
||||
setDescription("Generate new or enter existing");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getMnemonicWordsEntry(numWords));
|
||||
setContent(getMnemonicWordsEntry(numWords, true, true));
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
protected Node getMnemonicWordsEntry(int numWords) {
|
||||
protected Node getMnemonicWordsEntry(int numWords, boolean showPassphrase, boolean editPassphrase) {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
@@ -152,10 +196,12 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
vBox.getChildren().add(wordsPane);
|
||||
|
||||
PassphraseEntry passphraseEntry = new PassphraseEntry();
|
||||
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
|
||||
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
|
||||
vBox.getChildren().add(passphraseEntry);
|
||||
if(showPassphrase) {
|
||||
PassphraseEntry passphraseEntry = new PassphraseEntry(editPassphrase);
|
||||
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
|
||||
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
|
||||
vBox.getChildren().add(passphraseEntry);
|
||||
}
|
||||
|
||||
AnchorPane buttonPane = new AnchorPane();
|
||||
buttonPane.setPadding(new Insets(0, 26, 0, 10));
|
||||
@@ -250,9 +296,14 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
if(clipboard.hasString() && clipboard.getString().matches("(?m).+[\\n\\s][\\S\\s]*")) {
|
||||
String[] words = clipboard.getString().split("[\\n\\s]");
|
||||
WordEntry entry = WordEntry.this;
|
||||
for(String word : words) {
|
||||
for(int i = 0; i < words.length; i++) {
|
||||
String word = words[i];
|
||||
if(entry.nextField != null) {
|
||||
entry.nextField.requestFocus();
|
||||
if(i == words.length - 2 && isValid(word)) {
|
||||
label.requestFocus();
|
||||
} else {
|
||||
entry.nextField.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
entry.wordField.setText(word);
|
||||
@@ -282,7 +333,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
wordField.setTextFormatter(formatter);
|
||||
|
||||
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList, wordNumber, wordEntryList));
|
||||
autoCompletionBinding.setDelay(50);
|
||||
autoCompletionBinding.setOnAutoCompleted(event -> {
|
||||
if(nextField != null) {
|
||||
@@ -290,11 +341,20 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
}
|
||||
});
|
||||
|
||||
//Show autocomplete for the last word on focus if empty
|
||||
boolean lastWord = wordNumber == wordEntryList.size() - 1;
|
||||
if(lastWord) {
|
||||
wordField.focusedProperty().addListener((observable, oldValue, focused) -> {
|
||||
if(focused && wordField.getText().isEmpty()) {
|
||||
autoCompletionBinding.setUserInput("");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(wordField, Validator.combine(
|
||||
Validator.createEmptyValidator("Word is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", !wordList.contains(newValue))
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordList.contains(newValue))
|
||||
));
|
||||
|
||||
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
@@ -323,13 +383,30 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
|
||||
private final List<String> wordList;
|
||||
private final int wordNumber;
|
||||
private final ObservableList<String> wordEntryList;
|
||||
|
||||
public WordlistSuggestionProvider(List<String> wordList) {
|
||||
public WordlistSuggestionProvider(List<String> wordList, int wordNumber, ObservableList<String> wordEntryList) {
|
||||
this.wordList = wordList;
|
||||
this.wordNumber = wordNumber;
|
||||
this.wordEntryList = wordEntryList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
|
||||
if(wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
|
||||
try {
|
||||
List<String> possibleLastWords = Bip39MnemonicCode.INSTANCE.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText()));
|
||||
}
|
||||
|
||||
return possibleLastWords;
|
||||
} catch(Exception e) {
|
||||
log.warn("Cannot determine possible last words", e);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
for(String word : wordList) {
|
||||
@@ -341,26 +418,63 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
private boolean allPreviousWordsValid() {
|
||||
for(int i = 0; i < wordEntryList.size() - 1; i++) {
|
||||
if(!WordEntry.isValid(wordEntryList.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected class PassphraseEntry extends HBox {
|
||||
private final CustomTextField passphraseField;
|
||||
private final TextField passphraseField;
|
||||
|
||||
public PassphraseEntry() {
|
||||
public PassphraseEntry(boolean editable) {
|
||||
super();
|
||||
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setSpacing(10);
|
||||
Label usePassphraseLabel = new Label("Use passphrase?");
|
||||
usePassphraseLabel.managedProperty().bind(usePassphraseLabel.visibleProperty());
|
||||
CheckBox usePassphraseCheckbox = new CheckBox(" ");
|
||||
usePassphraseCheckbox.setDisable(!editable);
|
||||
usePassphraseCheckbox.managedProperty().bind(usePassphraseCheckbox.visibleProperty());
|
||||
usePassphraseLabel.visibleProperty().bind(usePassphraseCheckbox.visibleProperty());
|
||||
usePassphraseCheckbox.setVisible(passphraseProperty.isEmpty().get());
|
||||
usePassphraseCheckbox.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue) {
|
||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Add a passphrase?", "A passphrase is not a password! " +
|
||||
"Any variation entered in future loads a valid wallet, but with different addresses.\n\n" +
|
||||
"This feature provides optional added security for advanced users only. Are you sure?", ButtonType.NO, ButtonType.YES);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||
usePassphraseCheckbox.setVisible(false);
|
||||
} else {
|
||||
usePassphraseCheckbox.setSelected(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Label passphraseLabel = new Label("Passphrase:");
|
||||
passphraseField = (CustomTextField) TextFields.createClearableTextField();
|
||||
passphraseLabel.managedProperty().bind(passphraseLabel.visibleProperty());
|
||||
passphraseField = new TextField();
|
||||
passphraseField.setPromptText(passphraseProperty.isEmpty().get() ? "Leave blank for none" : "");
|
||||
passphraseField.setText(passphraseProperty.get());
|
||||
passphraseField.setDisable(!editable);
|
||||
passphraseField.managedProperty().bind(passphraseField.visibleProperty());
|
||||
passphraseLabel.visibleProperty().bind(passphraseField.visibleProperty());
|
||||
passphraseField.visibleProperty().bind(usePassphraseCheckbox.visibleProperty().not());
|
||||
passphraseProperty.bind(passphraseField.textProperty());
|
||||
passphraseField.setPromptText("Leave blank for none");
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setPrefHeight(28);
|
||||
helpLabel.setStyle("-fx-padding: 0 0 0 0");
|
||||
helpLabel.setHelpText("A passphrase provides optional added security - it is not stored so it must be remembered!");
|
||||
helpLabel.setHelpText("Advanced feature: a passphrase provides optional added security, but it is not stored so it must be remembered!");
|
||||
|
||||
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
|
||||
getChildren().addAll(usePassphraseLabel, usePassphraseCheckbox, passphraseLabel, passphraseField, helpLabel);
|
||||
}
|
||||
|
||||
public TextField getEditor() {
|
||||
|
||||
+7
-2
@@ -10,9 +10,11 @@ import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreMnemonicImport;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -135,7 +137,10 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
EventManager.get().post(new WalletImportEvent(optWallet.get()));
|
||||
} else {
|
||||
discoverButton.setDisable(false);
|
||||
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found", "Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
|
||||
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found",
|
||||
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
|
||||
"You can however import this wallet and scan the blockchain by supplying a start date. Do you want to import this wallet?" :
|
||||
"Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
setContent(getScriptTypeEntry());
|
||||
setExpanded(true);
|
||||
@@ -183,7 +188,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
scriptTypeComboBox.setMaxWidth(170);
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
|
||||
|
||||
Region region = new Region();
|
||||
|
||||
@@ -2,8 +2,11 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.PayNymImageLoadedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymService;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.concurrent.Service;
|
||||
@@ -121,9 +124,14 @@ public class PayNymAvatar extends StackPane {
|
||||
Proxy proxy = AppServices.getProxy();
|
||||
String url = PayNymService.getHostUrl(proxy != null) + "/" + paymentCodeStr + "/avatar";
|
||||
|
||||
if(log.isDebugEnabled()) {
|
||||
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);
|
||||
paymentCodeCache.put(cacheId, image);
|
||||
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
|
||||
return image;
|
||||
} catch(Exception e) {
|
||||
log.debug("Error loading PayNym avatar", e);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.io.Files;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
@@ -14,7 +15,10 @@ import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.CardApi;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.ActionEvent;
|
||||
@@ -42,7 +46,8 @@ import tornadofx.control.Form;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -96,6 +101,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
HBox.setHgrow(key, Priority.ALWAYS);
|
||||
keyField.getInputs().add(keyBox);
|
||||
|
||||
if(CardApi.isReaderAvailable()) {
|
||||
VBox cardButtonBox = new VBox(5);
|
||||
Button cardKey = new Button("", getGlyph(FontAwesome5.Glyph.WIFI));
|
||||
cardKey.setOnAction(event -> unsealPrivateKey());
|
||||
cardButtonBox.getChildren().add(cardKey);
|
||||
keyBox.getChildren().add(cardButtonBox);
|
||||
}
|
||||
|
||||
Field keyScriptTypeField = new Field();
|
||||
keyScriptTypeField.setText("Script Type:");
|
||||
keyScriptType = new ComboBox<>();
|
||||
@@ -279,6 +292,16 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
}
|
||||
}
|
||||
|
||||
private void unsealPrivateKey() {
|
||||
DeviceUnsealDialog deviceUnsealDialog = new DeviceUnsealDialog(Collections.emptyList());
|
||||
Optional<DeviceUnsealDialog.DevicePrivateKey> optPrivateKey = deviceUnsealDialog.showAndWait();
|
||||
if(optPrivateKey.isPresent()) {
|
||||
DeviceUnsealDialog.DevicePrivateKey devicePrivateKey = optPrivateKey.get();
|
||||
key.setText(devicePrivateKey.privateKey().getPrivateKeyEncoded().toBase58());
|
||||
keyScriptType.setValue(devicePrivateKey.scriptType());
|
||||
}
|
||||
}
|
||||
|
||||
private void createTransaction() {
|
||||
try {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
@@ -286,14 +309,30 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
Address fromAddress = scriptType.getAddress(privateKey.getKey());
|
||||
Address destAddress = getToAddress();
|
||||
|
||||
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress);
|
||||
Date since = null;
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
WalletBirthDateDialog addressScanDateDialog = new WalletBirthDateDialog(null, true);
|
||||
Optional<Date> optSince = addressScanDateDialog.showAndWait();
|
||||
if(optSince.isPresent()) {
|
||||
since = optSince.get();
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress, since);
|
||||
addressUtxosService.setOnSucceeded(successEvent -> {
|
||||
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
|
||||
});
|
||||
addressUtxosService.setOnFailed(failedEvent -> {
|
||||
Throwable rootCause = Throwables.getRootCause(failedEvent.getSource().getException());
|
||||
log.error("Error retrieving outputs for address " + fromAddress, failedEvent.getSource().getException());
|
||||
AppServices.showErrorDialog("Error retrieving outputs for address", failedEvent.getSource().getException().getMessage());
|
||||
AppServices.showErrorDialog("Error retrieving outputs for address", rootCause.getMessage());
|
||||
});
|
||||
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", "/image/sparrow.png", addressUtxosService);
|
||||
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
|
||||
}
|
||||
|
||||
addressUtxosService.start();
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating sweep transaction", e);
|
||||
@@ -321,8 +360,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
|
||||
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||
if(total - fee <= dustThreshold) {
|
||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||
return;
|
||||
feeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
|
||||
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1;
|
||||
|
||||
if(total - fee <= dustThreshold) {
|
||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Transaction transaction = new Transaction();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
public enum QRDensity {
|
||||
NORMAL("Normal", 250),
|
||||
LOW("Low", 80);
|
||||
|
||||
private final String name;
|
||||
private final int maxFragmentLength;
|
||||
|
||||
QRDensity(String name, int maxFragmentLength) {
|
||||
this.name = name;
|
||||
this.maxFragmentLength = maxFragmentLength;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getMaxFragmentLength() {
|
||||
return maxFragmentLength;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.sparrowwallet.hummingbird.LegacyUREncoder;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.UREncoder;
|
||||
@@ -28,13 +29,13 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class QRDisplayDialog extends Dialog<UR> {
|
||||
private static final Logger log = LoggerFactory.getLogger(QRDisplayDialog.class);
|
||||
|
||||
private static final int MIN_FRAGMENT_LENGTH = 10;
|
||||
private static final int MAX_FRAGMENT_LENGTH = 250;
|
||||
|
||||
private static final int ANIMATION_PERIOD_MILLIS = 200;
|
||||
|
||||
@@ -42,17 +43,20 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
private static final int QR_HEIGHT = 480;
|
||||
|
||||
private final UR ur;
|
||||
private final UREncoder encoder;
|
||||
private UREncoder encoder;
|
||||
|
||||
private final ImageView qrImageView;
|
||||
|
||||
private AnimateQRService animateQRService;
|
||||
private String currentPart;
|
||||
|
||||
private boolean addLegacyEncodingOption;
|
||||
private boolean useLegacyEncoding;
|
||||
private String[] legacyParts;
|
||||
private int legacyPartIndex;
|
||||
|
||||
private static boolean initialDensityChange;
|
||||
|
||||
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
|
||||
this(UR.fromBytes(type, data), addLegacyEncodingOption);
|
||||
}
|
||||
@@ -63,7 +67,8 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
|
||||
public QRDisplayDialog(UR ur, boolean addLegacyEncodingOption) {
|
||||
this.ur = ur;
|
||||
this.encoder = new UREncoder(ur, MAX_FRAGMENT_LENGTH, MIN_FRAGMENT_LENGTH, 0);
|
||||
this.addLegacyEncodingOption = addLegacyEncodingOption;
|
||||
this.encoder = new UREncoder(ur, Config.get().getQrDensity().getMaxFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
|
||||
|
||||
final DialogPane dialogPane = new QRDisplayDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
@@ -88,6 +93,9 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
if(addLegacyEncodingOption) {
|
||||
final ButtonType legacyEncodingButtonType = new javafx.scene.control.ButtonType("Use Legacy Encoding (Cobo Vault)", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(legacyEncodingButtonType);
|
||||
} else {
|
||||
final ButtonType densityButtonType = new javafx.scene.control.ButtonType("Change Density", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(densityButtonType);
|
||||
}
|
||||
|
||||
dialogPane.setPrefWidth(40 + QR_WIDTH + 40);
|
||||
@@ -201,6 +209,20 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
}
|
||||
}
|
||||
|
||||
private void changeQRDensity() {
|
||||
if(animateQRService != null) {
|
||||
animateQRService.cancel();
|
||||
}
|
||||
|
||||
this.encoder = new UREncoder(ur, Config.get().getQrDensity().getMaxFragmentLength(), MIN_FRAGMENT_LENGTH, 0);
|
||||
nextPart();
|
||||
if(encoder.isSinglePart()) {
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else {
|
||||
createAnimateQRService();
|
||||
}
|
||||
}
|
||||
|
||||
private class AnimateQRService extends ScheduledService<Boolean> {
|
||||
@Override
|
||||
protected Task<Boolean> createTask() {
|
||||
@@ -220,18 +242,44 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
ToggleButton legacy = new ToggleButton(buttonType.getText());
|
||||
legacy.setGraphicTextGap(5);
|
||||
setLegacyGraphic(legacy, false);
|
||||
if(addLegacyEncodingOption) {
|
||||
ToggleButton legacy = new ToggleButton(buttonType.getText());
|
||||
legacy.setGraphicTextGap(5);
|
||||
setLegacyGraphic(legacy, false);
|
||||
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(legacy, buttonData);
|
||||
legacy.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setUseLegacyEncoding(newValue);
|
||||
setLegacyGraphic(legacy, newValue);
|
||||
});
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(legacy, buttonData);
|
||||
legacy.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setUseLegacyEncoding(newValue);
|
||||
setLegacyGraphic(legacy, newValue);
|
||||
});
|
||||
|
||||
return legacy;
|
||||
return legacy;
|
||||
} else {
|
||||
Button density = new Button(buttonType.getText());
|
||||
density.setPrefWidth(160);
|
||||
density.setGraphicTextGap(5);
|
||||
updateDensityButton(density);
|
||||
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(density, buttonData);
|
||||
density.setOnAction(event -> {
|
||||
if(!initialDensityChange && !encoder.isSinglePart()) {
|
||||
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Discard progress?", "Changing the QR code density means any progress on the receiving device must be discarded. Proceed?", ButtonType.NO, ButtonType.YES);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
initialDensityChange = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Config.get().setQrDensity(Config.get().getQrDensity() == QRDensity.NORMAL ? QRDensity.LOW : QRDensity.NORMAL);
|
||||
updateDensityButton(density);
|
||||
changeQRDensity();
|
||||
});
|
||||
|
||||
return density;
|
||||
}
|
||||
}
|
||||
|
||||
return super.createButton(buttonType);
|
||||
@@ -245,10 +293,19 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
}
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
private void updateDensityButton(Button density) {
|
||||
density.setText(Config.get().getQrDensity() == QRDensity.NORMAL ? "Decrease Density" : "Increase Density");
|
||||
if(Config.get().getQrDensity() == QRDensity.NORMAL) {
|
||||
density.setGraphic(getGlyph(FontAwesome5.Glyph.MAGNIFYING_GLASS_PLUS));
|
||||
} else {
|
||||
density.setGraphic(getGlyph(FontAwesome5.Glyph.MAGNIFYING_GLASS_MINUS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@@ -259,7 +260,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
DeterministicSeed seed;
|
||||
try {
|
||||
extendedKey = ExtendedKey.fromDescriptor(qrtext);
|
||||
result = new Result(extendedKey);
|
||||
result = new Result(extendedKey, null);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//Ignore, not a valid xpub
|
||||
@@ -408,7 +409,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_HDKEY)) {
|
||||
CryptoHDKey cryptoHDKey = (CryptoHDKey)ur.decodeFromRegistry();
|
||||
ExtendedKey extendedKey = getExtendedKey(cryptoHDKey);
|
||||
return new Result(extendedKey);
|
||||
return new Result(extendedKey, cryptoHDKey.getName());
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_OUTPUT)) {
|
||||
CryptoOutput cryptoOutput = (CryptoOutput)ur.decodeFromRegistry();
|
||||
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
|
||||
@@ -542,7 +543,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
private KeyDerivation getKeyDerivation(CryptoKeypath cryptoKeypath) {
|
||||
if(cryptoKeypath != null) {
|
||||
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), cryptoKeypath.getPath());
|
||||
if(cryptoKeypath.getComponents().stream().anyMatch(PathComponent::isWildcard)) {
|
||||
throw new IllegalArgumentException("Wildcard derivation paths are not supported");
|
||||
}
|
||||
|
||||
List<ChildNumber> path = cryptoKeypath.getComponents().stream().map(comp -> new ChildNumber(comp.getIndex(), comp.isHardened())).collect(Collectors.toList());
|
||||
return new KeyDerivation(Utils.bytesToHex(cryptoKeypath.getSourceFingerprint()), KeyDerivation.writePath(path));
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -656,6 +662,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
public final PSBT psbt;
|
||||
public final BitcoinURI uri;
|
||||
public final ExtendedKey extendedKey;
|
||||
public final String extendedKeyName;
|
||||
public final OutputDescriptor outputDescriptor;
|
||||
public final List<Wallet> wallets;
|
||||
public final DeterministicSeed seed;
|
||||
@@ -667,6 +674,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.extendedKey = null;
|
||||
this.extendedKeyName = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
@@ -679,6 +687,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.psbt = psbt;
|
||||
this.uri = null;
|
||||
this.extendedKey = null;
|
||||
this.extendedKeyName = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
@@ -691,6 +700,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.psbt = null;
|
||||
this.uri = uri;
|
||||
this.extendedKey = null;
|
||||
this.extendedKeyName = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
@@ -703,6 +713,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.psbt = null;
|
||||
this.uri = BitcoinURI.fromAddress(address);
|
||||
this.extendedKey = null;
|
||||
this.extendedKeyName = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
@@ -710,11 +721,12 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
public Result(ExtendedKey extendedKey) {
|
||||
public Result(ExtendedKey extendedKey, String name) {
|
||||
this.transaction = null;
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.extendedKey = extendedKey;
|
||||
this.extendedKeyName = name;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
@@ -727,6 +739,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.extendedKey = null;
|
||||
this.extendedKeyName = null;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
@@ -739,6 +752,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.extendedKey = null;
|
||||
this.extendedKeyName = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = wallets;
|
||||
this.seed = null;
|
||||
@@ -751,6 +765,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.extendedKey = null;
|
||||
this.extendedKeyName = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = seed;
|
||||
@@ -763,6 +778,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.extendedKey = null;
|
||||
this.extendedKeyName = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
@@ -775,6 +791,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.extendedKey = null;
|
||||
this.extendedKeyName = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
|
||||
@@ -58,6 +58,8 @@ public class ScriptArea extends CodeArea {
|
||||
append("<signature" + signatureCount++ + ">", "script-signature");
|
||||
} else if(chunk.isPubKey()) {
|
||||
append("<pubkey" + pubKeyCount++ + ">", "script-pubkey");
|
||||
} else if(chunk.isString()) {
|
||||
append(chunk.toString(), "script-other");
|
||||
} else if(chunk.isScript()) {
|
||||
Script nestedScript = chunk.getScript();
|
||||
if (nestedScript.equals(redeemScript)) {
|
||||
|
||||
@@ -74,7 +74,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
results = new CoinTreeTable();
|
||||
results.setShowRoot(false);
|
||||
results.setPrefWidth(showWallet ? 950 : 850);
|
||||
results.setBitcoinUnit(walletForms.iterator().next().getWallet());
|
||||
results.setUnitFormat(walletForms.iterator().next().getWallet());
|
||||
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
results.setPlaceholder(new Label("No results"));
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SeedEntryDialog extends Dialog<List<String>> {
|
||||
private final MnemonicKeystoreEntryPane keystorePane;
|
||||
|
||||
public SeedEntryDialog(int numWords) {
|
||||
final DialogPane dialogPane = new MnemonicGridDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
int lines = numWords / 3;
|
||||
int height = lines * 40;
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
dialogPane.setContent(stackPane);
|
||||
|
||||
AnchorPane anchorPane = new AnchorPane();
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.getStyleClass().add("edge-to-edge");
|
||||
scrollPane.setPrefHeight(104 + height);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
anchorPane.getChildren().add(scrollPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
AnchorPane.setLeftAnchor(scrollPane, 0.0);
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
Accordion keystoreAccordion = new Accordion();
|
||||
scrollPane.setContent(keystoreAccordion);
|
||||
|
||||
keystorePane = new MnemonicKeystoreEntryPane(numWords);
|
||||
keystorePane.setAnimated(false);
|
||||
keystoreAccordion.getPanes().add(keystorePane);
|
||||
|
||||
stackPane.getChildren().addAll(anchorPane);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
Button okButton = (Button)dialogPane.lookupButton(ButtonType.OK);
|
||||
okButton.disableProperty().bind(keystorePane.validProperty().not());
|
||||
|
||||
final ButtonType generateButtonType = new javafx.scene.control.ButtonType("Generate New", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(generateButtonType);
|
||||
|
||||
setResultConverter((dialogButton) -> {
|
||||
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||
return data == ButtonBar.ButtonData.OK_DONE ? keystorePane.wordEntriesProperty.get() : null;
|
||||
});
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(180 + height);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Platform.runLater(() -> keystoreAccordion.setExpandedPane(keystorePane));
|
||||
}
|
||||
|
||||
private class MnemonicGridDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
Node button;
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
Button generateButton = new Button(buttonType.getText());
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(generateButton, buttonData);
|
||||
generateButton.setOnAction(event -> {
|
||||
keystorePane.generateNew();
|
||||
});
|
||||
|
||||
button = generateButton;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isGenerated() {
|
||||
return keystorePane.isGenerated();
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,13 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Optional;
|
||||
|
||||
public class TextAreaDialog extends Dialog<String> {
|
||||
@@ -22,6 +25,8 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
|
||||
private final TextArea textArea;
|
||||
private final String defaultValue;
|
||||
private final String fileName;
|
||||
private final byte[] fileBytes;
|
||||
|
||||
public TextAreaDialog() {
|
||||
this("");
|
||||
@@ -31,7 +36,11 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
this(defaultValue, true);
|
||||
}
|
||||
|
||||
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue, @NamedArg("editable") boolean editable) {
|
||||
public TextAreaDialog(String defaultValue, boolean editable) {
|
||||
this(defaultValue, editable, null, null);
|
||||
}
|
||||
|
||||
public TextAreaDialog(@NamedArg("defaultValue") String defaultValue, @NamedArg("editable") boolean editable, @NamedArg("fileName") String fileName, @NamedArg("fileBytes") byte[] fileBytes) {
|
||||
final DialogPane dialogPane = new TextAreaDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
|
||||
@@ -48,6 +57,8 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
HBox.setHgrow(this.textArea, Priority.ALWAYS);
|
||||
|
||||
this.defaultValue = defaultValue;
|
||||
this.fileName = fileName;
|
||||
this.fileBytes = fileBytes;
|
||||
|
||||
dialogPane.setContent(hbox);
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
@@ -60,6 +71,9 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
|
||||
final ButtonType scanButtonType = new javafx.scene.control.ButtonType("Scan QR", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(scanButtonType);
|
||||
} else if(fileBytes != null) {
|
||||
final ButtonType saveButtonType = new javafx.scene.control.ButtonType("Save File...", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(saveButtonType);
|
||||
}
|
||||
|
||||
Platform.runLater(textArea::requestFocus);
|
||||
@@ -86,7 +100,7 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
Node button;
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT && buttonType.getText().equals("Scan QR")) {
|
||||
Button scanButton = new Button(buttonType.getText());
|
||||
scanButton.setGraphicTextGap(5);
|
||||
scanButton.setGraphic(getGlyph(FontAwesome5.Glyph.CAMERA));
|
||||
@@ -118,6 +132,38 @@ public class TextAreaDialog extends Dialog<String> {
|
||||
});
|
||||
|
||||
button = scanButton;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT && buttonType.getText().equals("Save File...")) {
|
||||
Button saveButton = new Button(buttonType.getText());
|
||||
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(saveButton, buttonData);
|
||||
|
||||
saveButton.setOnAction(event -> {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save File");
|
||||
|
||||
if(fileName != null) {
|
||||
fileChooser.setInitialFileName(fileName);
|
||||
}
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
try {
|
||||
try(OutputStream outputStream = new FileOutputStream(file)) {
|
||||
outputStream.write(fileBytes);
|
||||
outputStream.flush();
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving file", e);
|
||||
AppServices.showErrorDialog("Error saving file", "Cannot write to " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
button = saveButton;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class TextfieldDialog extends Dialog<String> {
|
||||
private static final Logger log = LoggerFactory.getLogger(TextAreaDialog.class);
|
||||
|
||||
private final TextField textField;
|
||||
private final String defaultValue;
|
||||
|
||||
public TextfieldDialog() {
|
||||
this("");
|
||||
}
|
||||
|
||||
public TextfieldDialog(String defaultValue) {
|
||||
this(defaultValue, true);
|
||||
}
|
||||
|
||||
public TextfieldDialog(@NamedArg("defaultValue") String defaultValue, @NamedArg("editable") boolean editable) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
|
||||
Image image = new Image("/image/sparrow-small.png");
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
|
||||
HBox hbox = new HBox();
|
||||
this.textField = new TextField(defaultValue);
|
||||
this.textField.setMaxWidth(Double.MAX_VALUE);
|
||||
this.textField.setEditable(editable);
|
||||
hbox.getChildren().add(textField);
|
||||
HBox.setHgrow(this.textField, Priority.ALWAYS);
|
||||
|
||||
this.defaultValue = defaultValue;
|
||||
|
||||
dialogPane.setContent(hbox);
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
dialogPane.getStyleClass().add("text-input-dialog");
|
||||
dialogPane.getButtonTypes().add(ButtonType.OK);
|
||||
if(editable) {
|
||||
dialogPane.getButtonTypes().add(ButtonType.CANCEL);
|
||||
}
|
||||
|
||||
Platform.runLater(textField::requestFocus);
|
||||
|
||||
setResultConverter((dialogButton) -> {
|
||||
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||
return data == ButtonBar.ButtonData.OK_DONE ? textField.getText() : null;
|
||||
});
|
||||
|
||||
dialogPane.setPrefWidth(600);
|
||||
dialogPane.setPrefHeight(230);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
public final TextField getEditor() {
|
||||
return textField;
|
||||
}
|
||||
|
||||
public final String getDefaultValue() {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -106,13 +108,17 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
|
||||
protected void setDescription(String text) {
|
||||
descriptionLabel.getStyleClass().remove("description-error");
|
||||
descriptionLabel.getStyleClass().add("description-label");
|
||||
if(!descriptionLabel.getStyleClass().contains("description-label")) {
|
||||
descriptionLabel.getStyleClass().add("description-label");
|
||||
}
|
||||
descriptionLabel.setText(text);
|
||||
}
|
||||
|
||||
protected void setError(String title, String detail) {
|
||||
descriptionLabel.getStyleClass().remove("description-label");
|
||||
descriptionLabel.getStyleClass().add("description-error");
|
||||
if(!descriptionLabel.getStyleClass().contains("description-error")) {
|
||||
descriptionLabel.getStyleClass().add("description-error");
|
||||
}
|
||||
descriptionLabel.setText(title);
|
||||
if(detail != null && !detail.isEmpty()) {
|
||||
setContent(getContentBox(detail));
|
||||
@@ -159,4 +165,17 @@ public class TitledDescriptionPane extends TitledPane {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected static int getAccount(Wallet wallet, KeyDerivation requiredDerivation) {
|
||||
if(wallet == null || requiredDerivation == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int account = wallet.getScriptType().getAccount(requiredDerivation.getDerivationPath());
|
||||
if(account < 0) {
|
||||
account = 0;
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
@@ -33,15 +34,15 @@ public class TorStatusLabel extends Label {
|
||||
}
|
||||
|
||||
public void update() {
|
||||
boolean proxyInUse = AppServices.isUsingProxy();
|
||||
boolean internal = AppServices.isTorRunning();
|
||||
|
||||
if(!proxyInUse || internal) {
|
||||
if(!Config.get().isUseProxy()) {
|
||||
torConnectionTest.cancel();
|
||||
if(internal) {
|
||||
if(AppServices.isTorRunning()) {
|
||||
setTooltip(new Tooltip("Internal Tor proxy enabled"));
|
||||
}
|
||||
} else if(!torConnectionTest.isRunning()) {
|
||||
if(torConnectionTest.getState() == Worker.State.CANCELLED || torConnectionTest.getState() == Worker.State.FAILED) {
|
||||
torConnectionTest.reset();
|
||||
}
|
||||
torConnectionTest.setPeriod(Duration.seconds(20.0));
|
||||
torConnectionTest.setBackoffStrategy(null);
|
||||
torConnectionTest.setOnSucceeded(workerStateEvent -> {
|
||||
|
||||
@@ -3,10 +3,10 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
@@ -21,6 +21,7 @@ import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -28,15 +29,17 @@ import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.CubicCurve;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
@@ -45,11 +48,14 @@ import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.control.CoinLabel.BTC_FORMAT;
|
||||
|
||||
public class TransactionDiagram extends GridPane {
|
||||
private static final int MAX_UTXOS = 8;
|
||||
private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 2;
|
||||
@@ -69,13 +75,14 @@ public class TransactionDiagram extends GridPane {
|
||||
private final ObjectProperty<OptimizationStrategy> optimizationStrategyProperty = new SimpleObjectProperty<>(OptimizationStrategy.EFFICIENCY);
|
||||
private boolean expanded;
|
||||
private TransactionDiagram expandedDiagram;
|
||||
private ContextMenu contextMenu;
|
||||
|
||||
private final EventHandler<MouseEvent> expandedDiagramHandler = new EventHandler<>() {
|
||||
@Override
|
||||
public void handle(MouseEvent event) {
|
||||
if(!event.isConsumed() && event.getButton() != MouseButton.SECONDARY) {
|
||||
Stage stage = new Stage(StageStyle.UNDECORATED);
|
||||
stage.setTitle(walletTx.getPayments().iterator().next().getLabel());
|
||||
stage.setTitle(getDiagramTitle());
|
||||
stage.initOwner(TransactionDiagram.this.getScene().getWindow());
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.setResizable(false);
|
||||
@@ -98,7 +105,8 @@ public class TransactionDiagram extends GridPane {
|
||||
expandedDiagram = new TransactionDiagram();
|
||||
expandedDiagram.setId("transactionDiagram");
|
||||
expandedDiagram.setExpanded(true);
|
||||
updateExpandedDiagram();
|
||||
expandedDiagram.setFinal(isFinal());
|
||||
updateDerivedDiagram(expandedDiagram);
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
buttonBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -125,6 +133,13 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
};
|
||||
|
||||
private final EventHandler<ContextMenuEvent> contextMenuHandler = new EventHandler<>() {
|
||||
@Override
|
||||
public void handle(ContextMenuEvent event) {
|
||||
contextMenu.show(getChildren().iterator().next(), event.getScreenX(), event.getScreenY());
|
||||
}
|
||||
};
|
||||
|
||||
public void update(WalletTransaction walletTx) {
|
||||
setMinHeight(getDiagramHeight());
|
||||
setMaxHeight(getDiagramHeight());
|
||||
@@ -136,7 +151,7 @@ public class TransactionDiagram extends GridPane {
|
||||
update();
|
||||
setOnMouseClicked(expandedDiagramHandler);
|
||||
if(expandedDiagram != null) {
|
||||
updateExpandedDiagram();
|
||||
updateDerivedDiagram(expandedDiagram);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,20 +180,22 @@ public class TransactionDiagram extends GridPane {
|
||||
getChildren().clear();
|
||||
}
|
||||
|
||||
private void updateExpandedDiagram() {
|
||||
expandedDiagram.setFinal(isFinal());
|
||||
expandedDiagram.setOptimizationStrategy(getOptimizationStrategy());
|
||||
expandedDiagram.walletTx = walletTx;
|
||||
private void updateDerivedDiagram(TransactionDiagram diagram) {
|
||||
diagram.setOptimizationStrategy(getOptimizationStrategy());
|
||||
diagram.walletTx = walletTx;
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> utxoSets = expandedDiagram.getDisplayedUtxoSets();
|
||||
int maxSetSize = utxoSets.stream().mapToInt(Map::size).max().orElse(0);
|
||||
int maxRows = Math.max(maxSetSize * utxoSets.size(), walletTx.getPayments().size() + 2);
|
||||
double diagramHeight = Math.max(DIAGRAM_HEIGHT, Math.min(EXPANDED_DIAGRAM_HEIGHT, maxRows * ROW_HEIGHT));
|
||||
expandedDiagram.setMinHeight(diagramHeight);
|
||||
expandedDiagram.setMaxHeight(diagramHeight);
|
||||
expandedDiagram.update();
|
||||
if(diagram.isExpanded()) {
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> utxoSets = diagram.getDisplayedUtxoSets();
|
||||
int maxSetSize = utxoSets.stream().mapToInt(Map::size).max().orElse(0);
|
||||
int maxRows = Math.max(maxSetSize * utxoSets.size(), walletTx.getPayments().size() + 2);
|
||||
double diagramHeight = Math.max(DIAGRAM_HEIGHT, Math.min(EXPANDED_DIAGRAM_HEIGHT, maxRows * ROW_HEIGHT));
|
||||
diagram.setMinHeight(diagramHeight);
|
||||
diagram.setMaxHeight(diagramHeight);
|
||||
}
|
||||
|
||||
if(expandedDiagram.getScene() != null && expandedDiagram.getScene().getWindow() instanceof Stage stage) {
|
||||
diagram.update();
|
||||
|
||||
if(diagram.getScene() != null && diagram.getScene().getWindow() instanceof Stage stage) {
|
||||
stage.sizeToScene();
|
||||
}
|
||||
}
|
||||
@@ -208,6 +225,17 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
getChildren().clear();
|
||||
getChildren().addAll(inputsTypePane, inputsPane, inputsLinesPane, txPane, outputsLinesPane, outputsPane);
|
||||
|
||||
if(contextMenu == null) {
|
||||
contextMenu = new ContextMenu();
|
||||
MenuItem menuItem = new MenuItem("Save as Image...");
|
||||
menuItem.setOnAction(event -> {
|
||||
contextMenu.hide();
|
||||
saveAsImage();
|
||||
});
|
||||
contextMenu.getItems().add(menuItem);
|
||||
setOnContextMenuRequested(contextMenuHandler);
|
||||
}
|
||||
}
|
||||
|
||||
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
|
||||
@@ -434,7 +462,7 @@ public class TransactionDiagram extends GridPane {
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
joiner.add("Spending " + getSatsValue(inputValue) + " sats from" + (isExpanded() ? ":" : " (click to expand):"));
|
||||
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
|
||||
joiner.add(additionalInput.getHashAsString() + ":" + additionalInput.getIndex());
|
||||
joiner.add(getInputDescription(additionalInput));
|
||||
}
|
||||
tooltip.setText(joiner.toString());
|
||||
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
|
||||
@@ -797,12 +825,51 @@ public class TransactionDiagram extends GridPane {
|
||||
tooltip.setShowDuration(Duration.INDEFINITE);
|
||||
tooltip.getStyleClass().add("transaction-tooltip");
|
||||
txLabel.setTooltip(tooltip);
|
||||
|
||||
txPane.getChildren().add(txLabel);
|
||||
txPane.getChildren().add(createSpacer());
|
||||
|
||||
return txPane;
|
||||
}
|
||||
|
||||
private void saveAsImage() {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save Image");
|
||||
fileChooser.setInitialFileName(getDiagramTitle() + ".png");
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
TransactionDiagram transactionDiagram = new TransactionDiagram();
|
||||
transactionDiagram.setId("transactionDiagram");
|
||||
transactionDiagram.setFinal(true);
|
||||
transactionDiagram.setExpanded(isExpanded());
|
||||
transactionDiagram.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, null, null)));
|
||||
transactionDiagram.setStyle("-fx-text-background-color: " + (Config.get().getTheme() == Theme.DARK ? "#ffffff" : "#000000"));
|
||||
updateDerivedDiagram(transactionDiagram);
|
||||
Scene scene = new Scene(transactionDiagram);
|
||||
scene.setFill(Color.TRANSPARENT);
|
||||
scene.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
scene.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||
scene.getStylesheets().add(AppServices.class.getResource("wallet/send.css").toExternalForm());
|
||||
Image image = scene.snapshot(null);
|
||||
BufferedImage bufferedImage = SwingFXUtils.fromFXImage(image, null);
|
||||
try {
|
||||
ImageIO.write(bufferedImage, "png", file);
|
||||
} catch(IOException e) {
|
||||
AppServices.showErrorDialog("Error saving image", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getDiagramTitle() {
|
||||
if(!isFinal() && walletTx.getPayments().size() > 0 && walletTx.getPayments().get(0).getLabel() != null) {
|
||||
return walletTx.getPayments().get(0).getLabel();
|
||||
} else {
|
||||
return "[" + walletTx.getTransaction().getTxId().toString().substring(0, 6) + "]";
|
||||
}
|
||||
}
|
||||
|
||||
public double getDiagramHeight() {
|
||||
if(isExpanded()) {
|
||||
return getMaxHeight();
|
||||
@@ -1244,6 +1311,14 @@ public class TransactionDiagram extends GridPane {
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyAddress);
|
||||
|
||||
MenuItem showAddress = new MenuItem("Show Address as QR...");
|
||||
showAddress.setOnAction(event -> {
|
||||
hide();
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(address.toString());
|
||||
qrDisplayDialog.showAndWait();
|
||||
});
|
||||
getItems().add(showAddress);
|
||||
}
|
||||
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||
@@ -1257,7 +1332,8 @@ public class TransactionDiagram extends GridPane {
|
||||
copyBtcValue.setOnAction(event -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(BTC_FORMAT.format((double)value / Transaction.SATOSHIS_PER_BITCOIN));
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
content.putString(format.formatBtcValue(value));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().addAll(copySatsValue, copyBtcValue);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
||||
@@ -8,12 +7,10 @@ import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TransactionsTreeTable extends CoinTreeTable {
|
||||
public void initialize(WalletTransactionsEntry rootEntry) {
|
||||
getStyleClass().add("transactions-treetable");
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
updateAll(rootEntry);
|
||||
setShowRoot(false);
|
||||
@@ -58,7 +55,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
|
||||
}
|
||||
|
||||
public void updateAll(WalletTransactionsEntry rootEntry) {
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
setRoot(rootItem);
|
||||
|
||||
@@ -63,6 +63,8 @@ public class TrayManager {
|
||||
miStage.setFont(Font.decode(null).deriveFont(Font.BOLD));
|
||||
miStage.addActionListener(e -> Platform.runLater(() -> {
|
||||
stage.show();
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.setAlwaysOnTop(false);
|
||||
EventQueue.invokeLater(() -> {
|
||||
popupMenu.remove(miStage);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
@@ -36,8 +37,7 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
getData().add(utxoSeries);
|
||||
update(walletUtxosEntry);
|
||||
|
||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||
setBitcoinUnit(walletUtxosEntry.getWallet(), unit);
|
||||
setUnitFormat(walletUtxosEntry.getWallet(), Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void update(WalletUtxosEntry walletUtxosEntry) {
|
||||
@@ -117,12 +117,16 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
this.selectedEntries = entries;
|
||||
}
|
||||
|
||||
public void setBitcoinUnit(Wallet wallet, BitcoinUnit unit) {
|
||||
public void setUnitFormat(Wallet wallet, UnitFormat format, BitcoinUnit unit) {
|
||||
if(format == null) {
|
||||
format = UnitFormat.DOT;
|
||||
}
|
||||
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = wallet.getAutoUnit();
|
||||
}
|
||||
|
||||
NumberAxis yaxis = (NumberAxis)getYAxis();
|
||||
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, unit));
|
||||
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.scene.control.TableColumnBase;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class UtxosTreeTable extends CoinTreeTable {
|
||||
public void initialize(WalletUtxosEntry rootEntry) {
|
||||
getStyleClass().add("utxos-treetable");
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
updateAll(rootEntry);
|
||||
setShowRoot(false);
|
||||
@@ -93,7 +90,7 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
}
|
||||
|
||||
public void updateAll(WalletUtxosEntry rootEntry) {
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
setRoot(rootItem);
|
||||
|
||||
@@ -21,12 +21,12 @@ import java.util.Date;
|
||||
public class WalletBirthDateDialog extends Dialog<Date> {
|
||||
private final DatePicker birthDatePicker;
|
||||
|
||||
public WalletBirthDateDialog(Date birthDate) {
|
||||
public WalletBirthDateDialog(Date birthDate, boolean singleAddress) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle("Wallet Birth Date");
|
||||
dialogPane.setHeaderText("Select an approximate date earlier than the first wallet transaction:");
|
||||
setTitle(singleAddress ? "Address Scan Start Date" : "Wallet Birth Date");
|
||||
dialogPane.setHeaderText("Select an approximate date earlier than the first " + (singleAddress ? "" : "wallet") + " transaction:");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(420);
|
||||
@@ -58,7 +58,7 @@ public class WalletBirthDateDialog extends Dialog<Date> {
|
||||
));
|
||||
});
|
||||
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rescan Wallet", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType(singleAddress ? "Scan Address" : "Rescan Wallet", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> birthDatePicker.getValue() == null, birthDatePicker.valueProperty());
|
||||
|
||||
@@ -42,17 +42,19 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
|
||||
List<WalletExport> exporters;
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
exporters = List.of(new Electrum(), new Descriptor(), new SpecterDesktop(), new Sparrow());
|
||||
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels());
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
|
||||
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(), new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels());
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||
}
|
||||
|
||||
Accordion exportAccordion = new Accordion();
|
||||
for (WalletExport exporter : exporters) {
|
||||
FileWalletExportPane exportPane = new FileWalletExportPane(wallet, exporter);
|
||||
exportAccordion.getPanes().add(exportPane);
|
||||
for(WalletExport exporter : exporters) {
|
||||
if(!exporter.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileWalletExportPane exportPane = new FileWalletExportPane(wallet, exporter);
|
||||
exportAccordion.getPanes().add(exportPane);
|
||||
}
|
||||
}
|
||||
|
||||
exportAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ImageUtils;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.ImagePattern;
|
||||
import javafx.scene.shape.Circle;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
|
||||
public class WalletIcon extends StackPane {
|
||||
public static final String PROTOCOL = "walleticon";
|
||||
private static final String QUERY = "icon";
|
||||
public static final int WIDTH = 15;
|
||||
public static final int HEIGHT = 15;
|
||||
public static final int SAVE_WIDTH = WIDTH * 2;
|
||||
public static final int SAVE_HEIGHT = HEIGHT * 2;
|
||||
|
||||
private final Storage storage;
|
||||
private final ObjectProperty<Wallet> walletProperty = new SimpleObjectProperty<>();
|
||||
private final Keystore keystore;
|
||||
private final Glyph fallbackIcon;
|
||||
|
||||
public WalletIcon(Storage storage, Wallet wallet) {
|
||||
this(storage, wallet, null, null);
|
||||
}
|
||||
|
||||
public WalletIcon(Storage storage, Wallet wallet, Keystore keystore, Glyph fallbackIcon) {
|
||||
super();
|
||||
this.storage = storage;
|
||||
this.keystore = keystore;
|
||||
this.fallbackIcon = fallbackIcon;
|
||||
setPrefSize(WIDTH, HEIGHT);
|
||||
walletProperty.addListener((observable, oldValue, newValue) -> {
|
||||
refresh();
|
||||
});
|
||||
walletProperty.set(wallet);
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
Wallet wallet = getWallet();
|
||||
|
||||
getChildren().clear();
|
||||
if(wallet.getWalletConfig() != null && wallet.getWalletConfig().getIconData() != null) {
|
||||
String walletId = storage.getWalletId(wallet);
|
||||
if(AppServices.get().getWallet(walletId) != null) {
|
||||
addWalletIcon(walletId);
|
||||
} else {
|
||||
Platform.runLater(() -> addWalletIcon(walletId));
|
||||
}
|
||||
} else if(this.keystore != null || wallet.getKeystores().size() == 1) {
|
||||
Keystore keystore = this.keystore == null ? wallet.getKeystores().get(0) : this.keystore;
|
||||
if(keystore.getSource() == KeystoreSource.HW_USB || keystore.getSource() == KeystoreSource.HW_AIRGAPPED) {
|
||||
WalletModel walletModel = keystore.getWalletModel();
|
||||
|
||||
SVGImage svgImage;
|
||||
if(Config.get().getTheme() == Theme.DARK) {
|
||||
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon-invert.svg");
|
||||
} else {
|
||||
svgImage = loadSVGImage("/image/" + walletModel.getType() + "-icon.svg");
|
||||
}
|
||||
|
||||
if(svgImage != null) {
|
||||
getChildren().add(svgImage);
|
||||
return;
|
||||
}
|
||||
|
||||
Image image = null;
|
||||
if(Config.get().getTheme() == Theme.DARK) {
|
||||
image = loadImage("image/" + walletModel.getType() + "-icon-invert.png");
|
||||
}
|
||||
|
||||
if(image == null) {
|
||||
image = loadImage("image/" + walletModel.getType() + "-icon.png");
|
||||
}
|
||||
|
||||
if(image == null) {
|
||||
image = loadImage("image/" + walletModel.getType() + ".png");
|
||||
}
|
||||
|
||||
if(image != null && !image.isError()) {
|
||||
ImageView imageView = new ImageView(image);
|
||||
getChildren().add(imageView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(getChildren().isEmpty()) {
|
||||
Glyph glyph = fallbackIcon;
|
||||
if(glyph == null) {
|
||||
glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
|
||||
}
|
||||
glyph.setFontSize(10.0);
|
||||
getChildren().add(glyph);
|
||||
}
|
||||
}
|
||||
|
||||
private SVGImage loadSVGImage(String imageName) {
|
||||
try {
|
||||
URL url = AppServices.class.getResource(imageName);
|
||||
if(url != null) {
|
||||
return SVGLoader.load(url);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Image loadImage(String imageName) {
|
||||
try {
|
||||
return new Image(imageName, 15, 15, true, true);
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void addWalletIcon(String walletId) {
|
||||
Image image = new Image(PROTOCOL + ":" + walletId + "?" + QUERY, WIDTH, HEIGHT, true, false);
|
||||
getChildren().clear();
|
||||
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
|
||||
circle.setFill(new ImagePattern(image));
|
||||
getChildren().add(circle);
|
||||
}
|
||||
|
||||
public boolean addFailure() {
|
||||
if(getChildren().stream().noneMatch(node -> node.getStyleClass().contains("failure"))) {
|
||||
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
failGlyph.setFontSize(10);
|
||||
failGlyph.getStyleClass().add("failure");
|
||||
getChildren().add(failGlyph);
|
||||
StackPane.setAlignment(failGlyph, Pos.TOP_RIGHT);
|
||||
failGlyph.setTranslateX(5);
|
||||
failGlyph.setTranslateY(-4);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void removeFailure() {
|
||||
getChildren().removeIf(node -> node.getStyleClass().contains("failure"));
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return walletProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Wallet> walletProperty() {
|
||||
return walletProperty;
|
||||
}
|
||||
|
||||
public void setWallet(Wallet wallet) {
|
||||
this.walletProperty.set(wallet);
|
||||
}
|
||||
|
||||
public static class WalletIconStreamHandler extends URLStreamHandler {
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) throws IOException {
|
||||
return new URLConnection(url) {
|
||||
@Override
|
||||
public void connect() throws IOException {
|
||||
//Nothing required
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
String walletId = url.getPath();
|
||||
String query = url.getQuery();
|
||||
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
if(wallet == null) {
|
||||
throw new IOException("Cannot find wallet for wallet id " + walletId);
|
||||
}
|
||||
|
||||
if(query.startsWith(QUERY)) {
|
||||
if(wallet.getWalletConfig() == null || wallet.getWalletConfig().getIconData() == null) {
|
||||
throw new IOException("No icon data for " + walletId);
|
||||
}
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(wallet.getWalletConfig().getIconData());
|
||||
if(query.endsWith("@2x")) {
|
||||
return bais;
|
||||
} else {
|
||||
return ImageUtils.resize(bais, WalletIcon.WIDTH, WalletIcon.HEIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
throw new MalformedURLException("Cannot load url " + url);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
@@ -16,6 +17,7 @@ import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
@@ -24,7 +26,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
private final Accordion importAccordion;
|
||||
private final Button scanButton;
|
||||
|
||||
public WalletImportDialog() {
|
||||
public WalletImportDialog(List<WalletForm> selectedWalletForms) {
|
||||
EventManager.get().register(this);
|
||||
setOnCloseRequest(event -> {
|
||||
EventManager.get().unregister(this);
|
||||
@@ -49,16 +51,23 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
importAccordion = new Accordion();
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY());
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY());
|
||||
for(KeystoreFileImport importer : keystoreImporters) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
}
|
||||
}
|
||||
|
||||
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
|
||||
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()));
|
||||
if(!selectedWalletForms.isEmpty()) {
|
||||
walletImporters.add(new WalletLabels(selectedWalletForms));
|
||||
}
|
||||
for(WalletImport importer : walletImporters) {
|
||||
FileWalletImportPane importPane = new FileWalletImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileWalletImportPane importPane = new FileWalletImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
}
|
||||
}
|
||||
|
||||
importAccordion.getPanes().sort(Comparator.comparing(o -> ((TitledDescriptionPane) o).getTitle()));
|
||||
|
||||
@@ -34,6 +34,14 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
|
||||
}
|
||||
|
||||
public WalletNameDialog(String initialName) {
|
||||
this(initialName, false);
|
||||
}
|
||||
|
||||
public WalletNameDialog(String initialName, boolean hasExistingTransactions) {
|
||||
this(initialName, hasExistingTransactions, null);
|
||||
}
|
||||
|
||||
public WalletNameDialog(String initialName, boolean hasExistingTransactions, Date startDate) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
boolean requestBirthDate = (Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
|
||||
@@ -91,6 +99,12 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
|
||||
|
||||
if(requestBirthDate) {
|
||||
content.getChildren().add(existingBox);
|
||||
if(hasExistingTransactions) {
|
||||
existingCheck.setSelected(true);
|
||||
}
|
||||
if(startDate != null) {
|
||||
existingPicker.setValue(startDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
|
||||
}
|
||||
}
|
||||
|
||||
dialogPane.setContent(content);
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
|
||||
public class BitcoinUnitChangedEvent {
|
||||
public class BitcoinUnitChangedEvent extends UnitFormatChangedEvent {
|
||||
private final BitcoinUnit bitcoinUnit;
|
||||
|
||||
public BitcoinUnitChangedEvent(BitcoinUnit bitcoinUnit) {
|
||||
super(Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat());
|
||||
this.bitcoinUnit = bitcoinUnit;
|
||||
}
|
||||
|
||||
public BitcoinUnitChangedEvent(UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
|
||||
super(unitFormat);
|
||||
this.bitcoinUnit = bitcoinUnit;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
|
||||
import com.sparrowwallet.sparrow.net.TlsServerException;
|
||||
|
||||
public class ConnectionFailedEvent {
|
||||
@@ -20,6 +21,12 @@ public class ConnectionFailedEvent {
|
||||
|
||||
Throwable cause = (exception.getCause() != null ? exception.getCause() : exception);
|
||||
cause = (cause.getCause() != null ? cause.getCause() : cause);
|
||||
|
||||
if(cause instanceof JsonRpcException jsonRpcException && jsonRpcException.getErrorMessage() != null) {
|
||||
return jsonRpcException.getErrorMessage().getMessage() +
|
||||
(jsonRpcException.getErrorMessage().getData() != null ? " (" + jsonRpcException.getErrorMessage().getData().asText() + ")" : "");
|
||||
}
|
||||
|
||||
String message = splitCamelCase(cause.getClass().getSimpleName().replace("Exception", "Error"));
|
||||
return message + " (" + cause.getMessage() + ")";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class CormorantPruneStatusEvent extends CormorantStatusEvent {
|
||||
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd");
|
||||
|
||||
private final Wallet wallet;
|
||||
private final Date scanDate;
|
||||
private final Date pruneDate;
|
||||
private final boolean legacyWalletExists;
|
||||
|
||||
public CormorantPruneStatusEvent(String status, Wallet wallet, Date scanDate, Date pruneDate, boolean legacyWalletExists) {
|
||||
super(status);
|
||||
this.wallet = wallet;
|
||||
this.scanDate = scanDate;
|
||||
this.pruneDate = pruneDate;
|
||||
this.legacyWalletExists = legacyWalletExists;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFor(Wallet wallet) {
|
||||
return this.wallet == wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public Date getScanDate() {
|
||||
return scanDate;
|
||||
}
|
||||
|
||||
public String getScanDateAsString() {
|
||||
return DATE_FORMAT.format(scanDate);
|
||||
}
|
||||
|
||||
public Date getPruneDate() {
|
||||
return pruneDate;
|
||||
}
|
||||
|
||||
public String getPruneDateAsString() {
|
||||
return DATE_FORMAT.format(pruneDate);
|
||||
}
|
||||
|
||||
public boolean legacyWalletExists() {
|
||||
return legacyWalletExists;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Set;
|
||||
|
||||
public class CormorantScanStatusEvent extends CormorantStatusEvent {
|
||||
private final Set<Wallet> scanningWallets;
|
||||
private final int progress;
|
||||
private final Duration remainingDuration;
|
||||
|
||||
public CormorantScanStatusEvent(String status, Set<Wallet> scanningWallets, int progress, Duration remainingDuration) {
|
||||
super(status);
|
||||
this.scanningWallets = scanningWallets;
|
||||
this.progress = progress;
|
||||
this.remainingDuration = remainingDuration;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFor(Wallet wallet) {
|
||||
return scanningWallets.contains(wallet);
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return progress == 100;
|
||||
}
|
||||
|
||||
public Duration getRemaining() {
|
||||
return remainingDuration;
|
||||
}
|
||||
|
||||
public String getRemainingAsString() {
|
||||
if(remainingDuration != null) {
|
||||
if(progress < 30) {
|
||||
return Math.round((double)remainingDuration.toSeconds() / 60) + "m";
|
||||
} else {
|
||||
return remainingDuration.toMinutesPart() + "m " + remainingDuration.toSecondsPart() + "s";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public abstract class CormorantStatusEvent {
|
||||
private final String status;
|
||||
|
||||
public CormorantStatusEvent(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public abstract boolean isFor(Wallet wallet);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class CormorantSyncStatusEvent extends CormorantStatusEvent {
|
||||
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm");
|
||||
|
||||
private final int progress;
|
||||
private final Date tip;
|
||||
|
||||
public CormorantSyncStatusEvent(String status, int progress, Date tip) {
|
||||
super(status);
|
||||
this.progress = progress;
|
||||
this.tip = tip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFor(Wallet wallet) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return progress == 100;
|
||||
}
|
||||
|
||||
public Date getTip() {
|
||||
return tip;
|
||||
}
|
||||
|
||||
public String getTipAsString() {
|
||||
return tip == null ? "" : DATE_FORMAT.format(tip);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
|
||||
public class DeviceAddressEvent {
|
||||
private final Address address;
|
||||
|
||||
public DeviceAddressEvent(Address address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public Address getAddress() {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
|
||||
public class DeviceGetPrivateKeyEvent {
|
||||
private final ECKey privateKey;
|
||||
private final ScriptType scriptType;
|
||||
|
||||
public DeviceGetPrivateKeyEvent(ECKey privateKey, ScriptType scriptType) {
|
||||
this.privateKey = privateKey;
|
||||
this.scriptType = scriptType;
|
||||
}
|
||||
|
||||
public ECKey getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
|
||||
public ScriptType getScriptType() {
|
||||
return scriptType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class ExistingWalletImportedEvent {
|
||||
private final String existingWalletId;
|
||||
private final Wallet importedWallet;
|
||||
|
||||
public ExistingWalletImportedEvent(String existingWalletId, Wallet importedWallet) {
|
||||
this.existingWalletId = existingWalletId;
|
||||
this.importedWallet = importedWallet;
|
||||
}
|
||||
|
||||
public String getExistingWalletId() {
|
||||
return existingWalletId;
|
||||
}
|
||||
|
||||
public Wallet getImportedWallet() {
|
||||
return importedWallet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
|
||||
public class KeystoreExportEvent {
|
||||
private final Keystore keystore;
|
||||
|
||||
public KeystoreExportEvent(Keystore keystore) {
|
||||
this.keystore = keystore;
|
||||
}
|
||||
|
||||
public Keystore getKeystore() {
|
||||
return keystore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
|
||||
public class NewChildWalletSavedEvent {
|
||||
private final Storage storage;
|
||||
private final Wallet masterWallet;
|
||||
private final Wallet childWallet;
|
||||
|
||||
public NewChildWalletSavedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) {
|
||||
this.storage = storage;
|
||||
this.masterWallet = masterWallet;
|
||||
this.childWallet = childWallet;
|
||||
}
|
||||
|
||||
public String getMasterWalletId() {
|
||||
return storage.getWalletId(masterWallet);
|
||||
}
|
||||
|
||||
public Wallet getChildWallet() {
|
||||
return childWallet;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user