mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
68238e4e88 | ||
|
|
80fab6df99 | ||
|
|
6efe5e4ccc | ||
|
|
4c36d27d17 | ||
|
|
cc8dd59dbc | ||
|
|
7e91f57a42 | ||
|
|
f4c8bfa48c | ||
|
|
e0a14fdea6 | ||
|
|
b4af3586dc | ||
|
|
28722d385b | ||
|
|
dab6b9663a | ||
|
|
4e08334a3a | ||
|
|
dcb261a631 | ||
|
|
258fe34101 | ||
|
|
04917c45b6 | ||
|
|
08934d3c3c | ||
|
|
192657fa69 | ||
|
|
9c40c56c6c | ||
|
|
c4d0e4bac9 | ||
|
|
a62b14c8e4 | ||
|
|
3ec800e5e8 | ||
|
|
f30da06aaf | ||
|
|
03e9d23fa8 | ||
|
|
7dae9496ba | ||
|
|
b621cac18d | ||
|
|
13a576e871 | ||
|
|
19551671bd | ||
|
|
ecf9b78753 | ||
|
|
22303a2efc | ||
|
|
60aa20ac55 | ||
|
|
11cda40a40 | ||
|
|
ebbc4289e1 | ||
|
|
ac64811b35 | ||
|
|
7408744100 | ||
|
|
326fae88af | ||
|
|
0be73efdc1 | ||
|
|
486027f153 | ||
|
|
e42fc9a033 | ||
|
|
fc52670b2d | ||
|
|
4217de15a3 | ||
|
|
2d42ebff13 | ||
|
|
e629a51901 | ||
|
|
91273c2192 | ||
|
|
f8fce02a3d | ||
|
|
94c5920c27 | ||
|
|
c2eb505bd9 | ||
|
|
930e36fa2b | ||
|
|
9022438397 | ||
|
|
ba9aed5395 | ||
|
|
c780a8d944 | ||
|
|
3ed15ffefe | ||
|
|
ededb107a3 | ||
|
|
aebf4f9d28 | ||
|
|
336d0e551b | ||
|
|
5da9532614 | ||
|
|
956c2eaaaa | ||
|
|
62e7c34eb5 | ||
|
|
15da62777e | ||
|
|
3f2db7a199 | ||
|
|
adc9905038 | ||
|
|
fa82e1146b | ||
|
|
33a61e3414 | ||
|
|
70eda16515 | ||
|
|
e2eb7d3fa9 | ||
|
|
8aa0461d83 | ||
|
|
0f2cf9c5bb | ||
|
|
2c1f7e181c | ||
|
|
3555a0bd85 | ||
|
|
d7ce58d810 | ||
|
|
b0e1f6fe32 | ||
|
|
a324224e2a | ||
|
|
ddcb3e6f61 | ||
|
|
f4259642b8 | ||
|
|
db60afd13b | ||
|
|
f176a2a04f | ||
|
|
82be3a52dc | ||
|
|
4b2b8f653a | ||
|
|
555e5ecfb8 | ||
|
|
c0ca74ce6a | ||
|
|
674498052f | ||
|
|
1399b73dc2 | ||
|
|
c51f3d9e66 | ||
|
|
766a8c267f | ||
|
|
948d663fbf | ||
|
|
66be5c43a6 | ||
|
|
e0b00513b9 | ||
|
|
218761c594 | ||
|
|
5e4d6d5a78 | ||
|
|
b06df383dd | ||
|
|
361e92c600 | ||
|
|
69d0a2f96e | ||
|
|
bf078b2ea0 | ||
|
|
b5fa8f0ee0 | ||
|
|
7fb230e56b | ||
|
|
a53ecc4fdc | ||
|
|
3955eaaa3c | ||
|
|
d1d090a12b | ||
|
|
cd1509749a | ||
|
|
dd5278f442 | ||
|
|
631f5d48df | ||
|
|
c981cf32b9 | ||
|
|
984cabfc03 | ||
|
|
8d28f8f0a9 | ||
|
|
472fccc788 | ||
|
|
b1e715b272 | ||
|
|
6931cf7a45 | ||
|
|
1ccfc3c042 | ||
|
|
dd1976f173 | ||
|
|
eceaf40430 | ||
|
|
e565786bbc | ||
|
|
37a8a0a7f9 | ||
|
|
6aa3bb2ff3 | ||
|
|
516ee26ba0 | ||
|
|
1224abcd1d | ||
|
|
81eda96690 | ||
|
|
af9eb3cc64 | ||
|
|
d9bba16eb6 | ||
|
|
58cd50f674 | ||
|
|
b2b9dbeb8d | ||
|
|
0469141fee | ||
|
|
b16c7345a8 | ||
|
|
b1940e9293 | ||
|
|
1f51f632c4 | ||
|
|
ba199ff11b | ||
|
|
79c0f7769a | ||
|
|
761ec0659f | ||
|
|
5e31cdb7ac | ||
|
|
468384d82a | ||
|
|
230a4c5585 | ||
|
|
9048d341c6 | ||
|
|
b0f60bb671 | ||
|
|
9c87ecd4ec | ||
|
|
5324e5fcc2 | ||
|
|
c02da607e7 | ||
|
|
281fad5970 | ||
|
|
95d8201bd9 | ||
|
|
04cb27f85e | ||
|
|
a765e07c10 | ||
|
|
ef5cca26ea | ||
|
|
d86517606b | ||
|
|
9dcf3b7eea | ||
|
|
689f4abfde | ||
|
|
5357b55ef4 | ||
|
|
a10bdef484 | ||
|
|
58f20dab60 | ||
|
|
1c7abc1b24 | ||
|
|
02e0fd1357 | ||
|
|
7b3ff2a6d3 | ||
|
|
001e368775 | ||
|
|
aebc670b62 | ||
|
|
81810fced5 | ||
|
|
416fc83b4d | ||
|
|
e594007af1 | ||
|
|
3aee0acebb | ||
|
|
78a6ce4237 | ||
|
|
414c12aae4 | ||
|
|
724f9a5211 | ||
|
|
1a9c6f8f80 | ||
|
|
b640ffea44 | ||
|
|
6cf40e327c | ||
|
|
5959b00611 | ||
|
|
ce6b371206 | ||
|
|
e83c02653c | ||
|
|
487be2efb4 | ||
|
|
91d491f5ec | ||
|
|
9ec57b1ef6 | ||
|
|
cc31b5b78e | ||
|
|
c056b6240e | ||
|
|
cb8164c27f | ||
|
|
2ca286d826 | ||
|
|
dd7a3a6c8a | ||
|
|
7aeca7ebd3 | ||
|
|
4e4fd7501c | ||
|
|
a68eeb4669 | ||
|
|
ebf7a3f177 | ||
|
|
72b15967cc | ||
|
|
f75b4582c8 | ||
|
|
5d823571df | ||
|
|
cca61d281c | ||
|
|
34c9bc9b69 | ||
|
|
5fa048d242 | ||
|
|
ca928fc136 | ||
|
|
9bf53ab0cd | ||
|
|
542cc7de6f | ||
|
|
77fde3cda9 | ||
|
|
1eb595823b | ||
|
|
6d2167428f | ||
|
|
3820b9838d | ||
|
|
20a99e3236 | ||
|
|
c81f3d9f5d | ||
|
|
526de33bdd | ||
|
|
ee992b7255 | ||
|
|
7d459a9115 | ||
|
|
9faf036e4d | ||
|
|
306f241a4a | ||
|
|
41d1a1806d | ||
|
|
a825a693c1 | ||
|
|
7423d94935 | ||
|
|
82f9a0f2af | ||
|
|
7da62bb135 | ||
|
|
f2e5259916 | ||
|
|
4f4a48eb98 | ||
|
|
9c3b647f07 | ||
|
|
7f2d72ee59 | ||
|
|
548b8d270b | ||
|
|
696746f06c | ||
|
|
6f11a20feb | ||
|
|
c28c2ed506 | ||
|
|
8fb7f544de | ||
|
|
a76d9dba21 | ||
|
|
f1b3f7d5dd | ||
|
|
fd9e19d052 | ||
|
|
56363c083e | ||
|
|
bbdfec127a | ||
|
|
eb1087bf8d | ||
|
|
796f68640c | ||
|
|
ad091217d6 | ||
|
|
40e06b96a9 | ||
|
|
3fd186e22c | ||
|
|
e8c7f57704 | ||
|
|
e6de33034b | ||
|
|
73b6b9219b | ||
|
|
d1d1b0806f | ||
|
|
5ee97966ee | ||
|
|
4819f329ae | ||
|
|
52aed8a3f5 | ||
|
|
86ef129c1b | ||
|
|
aff872eea0 | ||
|
|
5d0025b4a7 | ||
|
|
e625a4e542 | ||
|
|
72ff1df61e | ||
|
|
3776fbafd9 | ||
|
|
cd91aff3bb | ||
|
|
b530ced9ed | ||
|
|
22957e9d88 | ||
|
|
d76aecb34d | ||
|
|
4cbb402931 | ||
|
|
4da82b110c | ||
|
|
880096a193 | ||
|
|
06c0fb8594 | ||
|
|
8e85543c6e |
@@ -7,36 +7,47 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest]
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 16.0.1
|
||||
uses: actions/setup-java@v2
|
||||
- name: Set up JDK 18.0.1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'adopt'
|
||||
java-version: '16.0.1'
|
||||
distribution: 'temurin'
|
||||
java-version: '18.0.1'
|
||||
- name: Show Build Versions
|
||||
run: ./gradlew -v
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v2
|
||||
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
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
if: ${{ runner.os == 'Windows' || runner.os == 'macOS' }}
|
||||
run: ./gradlew packageZipDistribution
|
||||
- name: Package tar distribution
|
||||
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/
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Sparrow is a modern desktop Bitcoin wallet application supporting most hardware wallets and built on common standards such as PSBT, with an emphasis on transparency and usability.
|
||||
|
||||
More information (and release binaries) can be found at https://sparrowwallet.com. Release binaries are also available directly from [Github](https://github.com/sparrowwallet/sparrow/releases).
|
||||
More information (and release binaries) can be found at https://sparrowwallet.com. Release binaries are also available directly from [GitHub](https://github.com/sparrowwallet/sparrow/releases).
|
||||
|
||||

|
||||
|
||||
@@ -16,8 +16,8 @@ or for those without SSH credentials:
|
||||
|
||||
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
|
||||
|
||||
In order to build, Sparrow requires Java 16 to be installed.
|
||||
The release binaries are built with [AdoptOpenJdk 16.0.1+9 Hotspot](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
|
||||
In order to build, Sparrow requires Java 17 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:
|
||||
|
||||
@@ -28,13 +28,15 @@ The Sparrow binaries can be built from source using
|
||||
|
||||
`./gradlew jpackage`
|
||||
|
||||
Note that to build the Windows installer, you will need to install [WiX](https://github.com/wixtoolset/wix3/releases).
|
||||
|
||||
When updating to the latest HEAD
|
||||
|
||||
`git pull --recurse-submodules`
|
||||
|
||||
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
|
||||
|
||||
@@ -42,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
|
||||
|
||||
`./sparrow`
|
||||
|
||||
Java 16 or higher must be installed.
|
||||
Java 17 or higher must be installed.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -75,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.
|
||||
|
||||
@@ -91,6 +93,12 @@ Please use the [Issues](https://github.com/sparrowwallet/sparrow/issues) tab abo
|
||||
|
||||
Sparrow is licensed under the Apache 2 software licence.
|
||||
|
||||
## GPG Key
|
||||
|
||||
The Sparrow release binaries here and on [sparrowwallet.com](https://sparrowwallet.com/download/) are signed using [craigraw's GPG key](https://keybase.io/craigraw):
|
||||
Fingerprint: D4D0D3202FC06849A257B38DE94618334C674B40
|
||||
64-bit: E946 1833 4C67 4B40
|
||||
|
||||
## Credit
|
||||
|
||||

|
||||
|
||||
+50
-28
@@ -1,16 +1,27 @@
|
||||
import java.awt.GraphicsEnvironment
|
||||
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'org.openjfx.javafxplugin' version '0.0.10'
|
||||
id 'org-openjfx-javafxplugin'
|
||||
id 'extra-java-module-info'
|
||||
id 'org.beryx.jlink' version '2.24.0'
|
||||
id 'org.beryx.jlink' version '2.25.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.5.3'
|
||||
def sparrowVersion = '1.7.0'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
osName = "osx"
|
||||
}
|
||||
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"))
|
||||
|
||||
group "com.sparrowwallet"
|
||||
version "${sparrowVersion}"
|
||||
@@ -29,7 +40,7 @@ tasks.withType(AbstractArchiveTask) {
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = "17"
|
||||
version = "18"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
@@ -45,13 +56,13 @@ dependencies {
|
||||
}
|
||||
implementation('com.google.guava:guava:28.2-jre')
|
||||
implementation('com.google.code.gson:gson:2.8.6')
|
||||
implementation('com.h2database:h2:2.0.202')
|
||||
implementation('com.h2database:h2:2.1.214')
|
||||
implementation('com.zaxxer:HikariCP:4.0.3')
|
||||
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0')
|
||||
implementation('org.flywaydb:flyway-core:7.10.6-SNAPSHOT')
|
||||
implementation('org.flywaydb:flyway-core:7.10.7-SNAPSHOT')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||
implementation('com.google.zxing:javase:3.4.0') {
|
||||
@@ -67,13 +78,13 @@ dependencies {
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.6.4')
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
|
||||
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
}
|
||||
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
|
||||
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
|
||||
exclude group: 'com.nativelibs4java', module: 'bridj'
|
||||
}
|
||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8") {
|
||||
implementation("com.sparrowwallet:netlayer-jpms-${osName}${targetName}:0.6.8") {
|
||||
exclude group: 'org.jetbrains.kotlin'
|
||||
}
|
||||
implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20')
|
||||
@@ -92,17 +103,19 @@ dependencies {
|
||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.24')
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.33')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.27')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.1')
|
||||
testImplementation('junit:junit:4.12')
|
||||
}
|
||||
|
||||
application {
|
||||
mainModule = 'com.sparrowwallet.sparrow'
|
||||
mainClass = 'com.sparrowwallet.sparrow.MainApp'
|
||||
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
||||
}
|
||||
|
||||
compileJava {
|
||||
@@ -117,17 +130,18 @@ compileJava {
|
||||
processResources {
|
||||
doLast {
|
||||
delete fileTree("$buildDir/resources/main/native").matching {
|
||||
exclude "${osName}/**"
|
||||
exclude "${osName}/${osArch}/**"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
|
||||
jvmArgs '--add-opens=java.base/java.io=com.google.gson'
|
||||
}
|
||||
|
||||
run {
|
||||
applicationDefaultJvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
"--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.behavior=org.controlsfx.controls",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control.inputmap=org.controlsfx.controls",
|
||||
@@ -150,6 +164,9 @@ run {
|
||||
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 {
|
||||
@@ -169,7 +186,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",
|
||||
@@ -196,11 +213,15 @@ jlink {
|
||||
"--add-reads=com.sparrowwallet.merged.module=logback.classic",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.databind",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.annotation",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core"]
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core",
|
||||
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor"]
|
||||
|
||||
if(os.macOsX) {
|
||||
jvmArgs += ["-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 {
|
||||
@@ -209,7 +230,7 @@ jlink {
|
||||
appVersion = "${sparrowVersion}"
|
||||
skipInstaller = os.macOsX || properties.skipInstallers
|
||||
imageOptions = []
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/aopp.properties', '--license-file', 'LICENSE']
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/auth47.properties', '--file-associations', 'src/main/deploy/lightning.properties', '--license-file', 'LICENSE']
|
||||
if(os.windows) {
|
||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
||||
@@ -236,12 +257,13 @@ task packageZipDistribution(type: Zip) {
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
from("$buildDir/jpackage/") {
|
||||
include "Sparrow/**"
|
||||
include "Sparrow.app/**"
|
||||
}
|
||||
}
|
||||
|
||||
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/") {
|
||||
@@ -326,12 +348,12 @@ extraJavaModuleInfo {
|
||||
requires('org.slf4j')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
}
|
||||
module('bridj-0.7-20140918-3.jar', 'com.nativelibs4java.bridj', '0.7-20140918-3') {
|
||||
module("bridj${targetName}-0.7-20140918-3.jar", 'com.nativelibs4java.bridj', '0.7-20140918-3') {
|
||||
exports('org.bridj')
|
||||
exports('org.bridj.cpp')
|
||||
requires('java.logging')
|
||||
}
|
||||
module('webcam-capture-0.3.13-SNAPSHOT.jar', 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
|
||||
module("webcam-capture${targetName}-0.3.13-SNAPSHOT.jar", 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
|
||||
exports('com.github.sarxos.webcam')
|
||||
exports('com.github.sarxos.webcam.ds.buildin')
|
||||
exports('com.github.sarxos.webcam.ds.buildin.natives')
|
||||
@@ -359,7 +381,7 @@ extraJavaModuleInfo {
|
||||
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
|
||||
exports('org.json.simple')
|
||||
}
|
||||
module('logback-classic-1.2.3.jar', 'logback.classic', '1.2.3') {
|
||||
module('logback-classic-1.2.8.jar', 'logback.classic', '1.2.8') {
|
||||
exports('ch.qos.logback.classic')
|
||||
requires('org.slf4j')
|
||||
requires('logback.core')
|
||||
@@ -450,6 +472,8 @@ extraJavaModuleInfo {
|
||||
module('kotlin-stdlib-jdk7-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk7', '1.5.20')
|
||||
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
|
||||
exports('kotlin')
|
||||
exports('kotlin.jvm')
|
||||
exports('kotlin.collections')
|
||||
}
|
||||
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
|
||||
exports('com.sparrowwallet.hummingbird')
|
||||
@@ -459,7 +483,7 @@ extraJavaModuleInfo {
|
||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||
exports('co.nstant.in.cbor')
|
||||
}
|
||||
module('nightjar-0.2.24.jar', 'com.sparrowwallet.nightjar', '0.2.24') {
|
||||
module('nightjar-0.2.33.jar', 'com.sparrowwallet.nightjar', '0.2.33') {
|
||||
requires('com.google.common')
|
||||
requires('net.sourceforge.streamsupport')
|
||||
requires('org.slf4j')
|
||||
@@ -505,6 +529,7 @@ extraJavaModuleInfo {
|
||||
exports('com.samourai.whirlpool.protocol.rest')
|
||||
exports('com.samourai.whirlpool.client.tx0')
|
||||
exports('com.samourai.wallet.segwit.bech32')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.chain')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.wallet')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.minerFee')
|
||||
exports('com.samourai.whirlpool.client.wallet.data.walletState')
|
||||
@@ -546,7 +571,7 @@ extraJavaModuleInfo {
|
||||
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
|
||||
exports('net.jcip.annotations')
|
||||
}
|
||||
module("netlayer-jpms-${osName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
|
||||
module("netlayer-jpms-${osName}${targetName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
|
||||
exports('org.berndpruenster.netlayer.tor')
|
||||
requires('com.github.ravn.jsocks')
|
||||
requires('com.github.JesusMcCloud.jtorctl')
|
||||
@@ -571,18 +596,15 @@ extraJavaModuleInfo {
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
|
||||
module('logback-core-1.2.3.jar', 'logback.core', '1.2.3') {
|
||||
module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
|
||||
requires('java.xml')
|
||||
}
|
||||
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
|
||||
exports('kotlin.jvm')
|
||||
exports('kotlin.collections')
|
||||
}
|
||||
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
|
||||
exports('com.beust.jcommander')
|
||||
}
|
||||
module('junit-4.12.jar', 'junit', '4.12') {
|
||||
exports('org.junit')
|
||||
requires('org.hamcrest.core')
|
||||
}
|
||||
module('hamcrest-core-1.3.jar', 'org.hamcrest.core', '1.3')
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -67,6 +67,8 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
|
||||
outputs.file(originalJar);
|
||||
} else if (automaticModules.containsKey(originalJarName)) {
|
||||
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
|
||||
} else if(originalJarName.startsWith("kotlin-stdlib-common")) {
|
||||
//ignore
|
||||
} else {
|
||||
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
+59
-26
@@ -1,58 +1,88 @@
|
||||
## 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.5.0 and later, this is AdoptOpenJdk jdk-16.0.1+9 Hotspot.
|
||||
It is available for all supported platforms from the [AdoptOpenJdk site](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
|
||||
For v1.6.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/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_linux_hotspot_16.0.1_9.tar.gz)
|
||||
- [MacOS x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_mac_hotspot_16.0.1_9.tar.gz)
|
||||
- [Windows x64](https://github.com/AdoptOpenJDK/openjdk16-binaries/releases/download/jdk-16.0.1%2B9/OpenJDK16U-jdk_x64_windows_hotspot_16.0.1_9.zip)
|
||||
- [Linux x64](https://github.com/adoptium/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 https://adoptopenjdk.jfrog.io/adoptopenjdk/api/gpg/key/public
|
||||
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --import public
|
||||
gpg --no-default-keyring --keyring ./adoptopenjdk-keyring.gpg --export --output adoptopenjdk-archive-keyring.gpg
|
||||
rm adoptopenjdk-keyring.gpg
|
||||
sudo mv adoptopenjdk-archive-keyring.gpg /usr/share/keyrings
|
||||
echo "deb [signed-by=/usr/share/keyrings/adoptopenjdk-archive-keyring.gpg] https://adoptopenjdk.jfrog.io/adoptopenjdk/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/adoptopenjdk.list
|
||||
sudo apt update -y
|
||||
sudo apt-get install -y adoptopenjdk-16-hotspot=16.0.1+9-3
|
||||
|
||||
- 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-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 16.0.1.hs-adpt
|
||||
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
|
||||
|
||||
The project can cloned for a specific release tag as follows:
|
||||
```shell
|
||||
GIT_TAG="1.5.2"
|
||||
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
|
||||
GIT_TAG="1.7.0"
|
||||
git clone --recursive --branch "${GIT_TAG}" https://github.com/sparrowwallet/sparrow.git
|
||||
```
|
||||
|
||||
Thereafter, building should be straightforward:
|
||||
@@ -64,7 +94,9 @@ cd sparrow
|
||||
|
||||
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`).
|
||||
@@ -76,3 +108,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: 20ec0f95fb...7c34ec7c3b
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright 2015 the original author or authors.
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@@ -17,67 +17,101 @@
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||
# noncompliant, but you have some other compliant shell such as ksh or
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
# * functions;
|
||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||
# * compound commands having a testable exit status, especially «case»;
|
||||
# * various built-in commands including «command», «set», and «ulimit».
|
||||
#
|
||||
# Important for patching:
|
||||
#
|
||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||
#
|
||||
# The "traditional" practice of packing multiple parameters into a
|
||||
# space-separated string is a well documented source of bugs and security
|
||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||
# options in "$@", and eventually passing that to Java.
|
||||
#
|
||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||
# see the in-line comments for details.
|
||||
#
|
||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
APP_BASE_NAME=${0##*/}
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
@@ -106,80 +140,101 @@ location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=`expr $i + 1`
|
||||
done
|
||||
case $i in
|
||||
0) set -- ;;
|
||||
1) set -- "$args0" ;;
|
||||
2) set -- "$args0" "$args1" ;;
|
||||
3) set -- "$args0" "$args1" "$args2" ;;
|
||||
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --module-path (only if needed)
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
if "$cygwin" || "$msys" ; then
|
||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||
|
||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
for arg do
|
||||
if
|
||||
case $arg in #(
|
||||
-*) false ;; # don't mess with options #(
|
||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||
[ -e "$t" ] ;; #(
|
||||
*) false ;;
|
||||
esac
|
||||
then
|
||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||
fi
|
||||
# Roll the args list around exactly as many times as the number of
|
||||
# args, so each arg winds up back in the position where it started, but
|
||||
# possibly modified.
|
||||
#
|
||||
# NB: a `for` loop captures its iteration list before it begins, so
|
||||
# changing the positional parameters here affects neither the number of
|
||||
# iterations, nor the values presented in `arg`.
|
||||
shift # remove old arg
|
||||
set -- "$@" "$arg" # push replacement arg
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
if ! command -v xargs >/dev/null 2>&1
|
||||
then
|
||||
die "xargs is not available"
|
||||
fi
|
||||
|
||||
# Use "xargs" to parse quoted args.
|
||||
#
|
||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||
#
|
||||
# In Bash we could simply go:
|
||||
#
|
||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||
# set -- "${ARGS[@]}" "$@"
|
||||
#
|
||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||
# character that might be a shell metacharacter, then use eval to reverse
|
||||
# that process (while maintaining the separation between arguments), and wrap
|
||||
# the whole thing up as a single "set" statement.
|
||||
#
|
||||
# This will of course break if any of these variables contains a newline or
|
||||
# an unmatched quote.
|
||||
#
|
||||
|
||||
eval "set -- $(
|
||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||
xargs -n1 |
|
||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
|
||||
Vendored
+8
-6
@@ -14,7 +14,7 @@
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@@ -25,7 +25,7 @@
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
@@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
mime-type=x-scheme-handler/aopp
|
||||
description=Verify Address Ownership URI
|
||||
@@ -0,0 +1,2 @@
|
||||
mime-type=x-scheme-handler/auth47
|
||||
description=Auth47 Authentication URI
|
||||
@@ -0,0 +1,2 @@
|
||||
mime-type=x-scheme-handler/lightning
|
||||
description=LNURL URI
|
||||
@@ -6,4 +6,4 @@ Icon=/opt/sparrow/lib/Sparrow.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Unknown
|
||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/aopp
|
||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.5.3</string>
|
||||
<string>1.7.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
@@ -47,10 +47,18 @@
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.sparrowwallet.sparrow.aopp</string>
|
||||
<string>com.sparrowwallet.sparrow.auth47</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>aopp</string>
|
||||
<string>auth47</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.sparrowwallet.sparrow.lightning</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>lightning</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
@@ -77,9 +77,9 @@
|
||||
|
||||
<DirectoryRef Id="TARGETDIR">
|
||||
<Component Id="RegistryEntries" Guid="{206C911C-56EF-44B8-9257-5FD214427965}">
|
||||
<RegistryKey Root="HKCR" Key="aopp" Action="createAndRemoveOnUninstall">
|
||||
<RegistryKey Root="HKCR" Key="auth47" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:Address Ownership Proof Protocol"/>
|
||||
<RegistryValue Type="string" Value="URL:Auth47 Authentication URI"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
@@ -97,6 +97,16 @@
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
<RegistryKey Root="HKCR" Key="lightning" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:LNURL URI"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,21 +3,31 @@ package com.sparrowwallet.sparrow;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.crypto.Key;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.Auth47;
|
||||
import com.sparrowwallet.drongo.protocol.BlockHeader;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.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;
|
||||
@@ -32,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;
|
||||
@@ -42,7 +51,7 @@ 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;
|
||||
|
||||
@@ -58,8 +67,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 {
|
||||
@@ -67,25 +74,37 @@ public class AppServices {
|
||||
|
||||
private static final int SERVER_PING_PERIOD_SECS = 60;
|
||||
private static final int PUBLIC_SERVER_RETRY_PERIOD_SECS = 3;
|
||||
private static final int ENUMERATE_HW_PERIOD_SECS = 30;
|
||||
private static final int PRIVATE_SERVER_RETRY_PERIOD_SECS = 15;
|
||||
public static final int ENUMERATE_HW_PERIOD_SECS = 30;
|
||||
private static final int RATES_PERIOD_SECS = 5 * 60;
|
||||
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
|
||||
private static final int CONNECTION_DELAY_SECS = 2;
|
||||
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
|
||||
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
|
||||
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
||||
|
||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
||||
public static final List<Long> FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L);
|
||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||
|
||||
private static AppServices INSTANCE;
|
||||
|
||||
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
|
||||
|
||||
private final SorobanServices sorobanServices = new SorobanServices();
|
||||
|
||||
private final MainApp application;
|
||||
private InteractionServices interactionServices;
|
||||
|
||||
private static PayNymService payNymService;
|
||||
|
||||
private final Application application;
|
||||
|
||||
private final Map<Window, List<WalletTabData>> walletWindows = new LinkedHashMap<>();
|
||||
|
||||
private TrayManager trayManager;
|
||||
|
||||
private static Image windowIcon;
|
||||
|
||||
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
private ExchangeSource.RatesService ratesService;
|
||||
@@ -106,7 +125,7 @@ public class AppServices {
|
||||
|
||||
private static Map<Integer, Double> targetBlockFeeRates;
|
||||
|
||||
private static final Map<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
||||
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
||||
|
||||
private static Double minimumRelayFeeRate;
|
||||
|
||||
@@ -130,7 +149,9 @@ public class AppServices {
|
||||
torService.reset();
|
||||
}
|
||||
|
||||
torService.start();
|
||||
if(torService.getState() != Worker.State.RUNNING) {
|
||||
torService.start();
|
||||
}
|
||||
} else {
|
||||
restartServices();
|
||||
}
|
||||
@@ -150,8 +171,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);
|
||||
@@ -180,7 +202,7 @@ public class AppServices {
|
||||
|
||||
private void restartServices() {
|
||||
Config config = Config.get();
|
||||
if(config.hasServerAddress()) {
|
||||
if(config.hasServer()) {
|
||||
restartService(connectionService);
|
||||
}
|
||||
|
||||
@@ -188,7 +210,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);
|
||||
}
|
||||
|
||||
@@ -224,6 +246,11 @@ public class AppServices {
|
||||
versionCheckService.cancel();
|
||||
}
|
||||
|
||||
if(payNymService != null) {
|
||||
PayNymService.ShutdownService shutdownService = new PayNymService.ShutdownService(payNymService);
|
||||
shutdownService.start();
|
||||
}
|
||||
|
||||
if(Tor.getDefault() != null) {
|
||||
Tor.getDefault().shutdown();
|
||||
}
|
||||
@@ -231,13 +258,16 @@ public class AppServices {
|
||||
|
||||
private ElectrumServer.ConnectionService createConnectionService() {
|
||||
ElectrumServer.ConnectionService connectionService = new ElectrumServer.ConnectionService();
|
||||
//Delay startup on first connection to Bitcoin Core to allow any unencrypted wallets to open first
|
||||
connectionService.setDelay(Config.get().getServerType() == ServerType.BITCOIN_CORE ? Duration.seconds(CONNECTION_DELAY_SECS) : Duration.ZERO);
|
||||
connectionService.setPeriod(Duration.seconds(SERVER_PING_PERIOD_SECS));
|
||||
connectionService.setRestartOnFailure(true);
|
||||
EventManager.get().register(connectionService);
|
||||
|
||||
connectionService.setOnRunning(workerStateEvent -> {
|
||||
connectionService.setDelay(Duration.ZERO);
|
||||
if(!ElectrumServer.isConnected()) {
|
||||
EventManager.get().post(new ConnectionStartEvent(Config.get().getServerAddress()));
|
||||
EventManager.get().post(new ConnectionStartEvent(Config.get().getServerDisplayName()));
|
||||
}
|
||||
});
|
||||
connectionService.setOnSucceeded(successEvent -> {
|
||||
@@ -276,9 +306,12 @@ public class AppServices {
|
||||
"\n\nThis 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) {
|
||||
crtFile.delete();
|
||||
Platform.runLater(() -> restartService(connectionService));
|
||||
return;
|
||||
if(crtFile.delete()) {
|
||||
Platform.runLater(() -> restartService(connectionService));
|
||||
return;
|
||||
} else {
|
||||
AppServices.showErrorDialog("Could not delete certificate", "The certificate file at " + crtFile.getAbsolutePath() + " could not be deleted.\n\nPlease delete this file manually.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,6 +331,8 @@ public class AppServices {
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
|
||||
Config.get().changePublicServer();
|
||||
connectionService.setPeriod(Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS));
|
||||
} else {
|
||||
connectionService.setPeriod(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
|
||||
}
|
||||
|
||||
log.debug("Connection failed", failEvent.getSource().getException());
|
||||
@@ -339,7 +374,7 @@ public class AppServices {
|
||||
|
||||
private Hwi.ScheduledEnumerateService createDeviceEnumerateService() {
|
||||
Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null);
|
||||
enumerateService.setPeriod(Duration.seconds(ENUMERATE_HW_PERIOD_SECS));
|
||||
enumerateService.setPeriod(Duration.seconds(Config.get().getEnumerateHwPeriod()));
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
List<Device> devices = enumerateService.getValue();
|
||||
|
||||
@@ -463,8 +498,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() {
|
||||
@@ -479,6 +518,30 @@ public class AppServices {
|
||||
return get().sorobanServices;
|
||||
}
|
||||
|
||||
public static InteractionServices getInteractionServices() {
|
||||
return get().interactionServices;
|
||||
}
|
||||
|
||||
public static PayNymService getPayNymService() {
|
||||
if(payNymService == null) {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
payNymService = new PayNymService(torProxy);
|
||||
} else {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(!Objects.equals(payNymService.getTorProxy(), torProxy)) {
|
||||
payNymService.setTorProxy(getTorProxy());
|
||||
}
|
||||
}
|
||||
|
||||
return payNymService;
|
||||
}
|
||||
|
||||
public static HostAndPort getTorProxy() {
|
||||
return AppServices.isTorRunning() ?
|
||||
HostAndPort.fromParts("localhost", TorService.PROXY_PORT) :
|
||||
(Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer()));
|
||||
}
|
||||
|
||||
public static AppController newAppWindow(Stage stage) {
|
||||
try {
|
||||
FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml"));
|
||||
@@ -492,7 +555,7 @@ public class AppServices {
|
||||
stage.setMinWidth(650);
|
||||
stage.setMinHeight(708);
|
||||
stage.setScene(scene);
|
||||
stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow-large.png")));
|
||||
stage.getIcons().add(getWindowIcon());
|
||||
|
||||
appController.initializeView();
|
||||
stage.show();
|
||||
@@ -503,11 +566,19 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
private static Image getWindowIcon() {
|
||||
if(windowIcon == null) {
|
||||
windowIcon = new Image(SparrowWallet.class.getResourceAsStream("/image/sparrow-icon.png"));
|
||||
}
|
||||
|
||||
return windowIcon;
|
||||
}
|
||||
|
||||
public static boolean isReducedWindowHeight(Node node) {
|
||||
return (node.getScene() != null && node.getScene().getWindow().getHeight() < 768);
|
||||
}
|
||||
|
||||
public MainApp getApplication() {
|
||||
public Application getApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
@@ -543,6 +614,18 @@ public class AppServices {
|
||||
return getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public WalletTransaction getCreatedTransaction(Set<BlockTransactionHashIndex> utxos) {
|
||||
for(List<WalletTabData> walletTabDataList : walletWindows.values()) {
|
||||
for(WalletTabData walletTabData : walletTabDataList) {
|
||||
if(walletTabData.getWalletForm().getCreatedWalletTransaction() != null && utxos.equals(walletTabData.getWalletForm().getCreatedWalletTransaction().getSelectedUtxos().keySet())) {
|
||||
return walletTabData.getWalletForm().getCreatedWalletTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Window getWindowForWallet(String walletId) {
|
||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getWalletForm().getWalletId().equals(walletId))).map(Map.Entry::getKey).findFirst();
|
||||
return optWindow.orElse(null);
|
||||
@@ -577,11 +660,22 @@ public class AppServices {
|
||||
return latestBlockHeader;
|
||||
}
|
||||
|
||||
public static Double getDefaultFeeRate() {
|
||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||
return getTargetBlockFeeRates() == null ? FALLBACK_FEE_RATE : getTargetBlockFeeRates().get(defaultTarget);
|
||||
}
|
||||
|
||||
public static Double getMinimumFeeRate() {
|
||||
Optional<Double> optMinFeeRate = getTargetBlockFeeRates().values().stream().min(Double::compareTo);
|
||||
Double minRate = optMinFeeRate.orElse(FALLBACK_FEE_RATE);
|
||||
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||
}
|
||||
|
||||
public static Map<Integer, Double> getTargetBlockFeeRates() {
|
||||
return targetBlockFeeRates;
|
||||
}
|
||||
|
||||
public static Map<Date, Set<MempoolRateSize>> getMempoolHistogram() {
|
||||
public static TreeMap<Date, Set<MempoolRateSize>> getMempoolHistogram() {
|
||||
return mempoolHistogram;
|
||||
}
|
||||
|
||||
@@ -592,6 +686,9 @@ public class AppServices {
|
||||
}
|
||||
|
||||
mempoolHistogram.put(Date.from(dateMinute.atZone(ZoneId.systemDefault()).toInstant()), rateSizes);
|
||||
|
||||
Date yesterday = Date.from(LocalDateTime.now().minusDays(1).atZone(ZoneId.systemDefault()).toInstant());
|
||||
mempoolHistogram.keySet().removeIf(date -> date.before(yesterday));
|
||||
}
|
||||
|
||||
public static Double getMinimumRelayFeeRate() {
|
||||
@@ -617,8 +714,18 @@ public class AppServices {
|
||||
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
||||
}
|
||||
|
||||
public static void clearPayjoinURI(Address address) {
|
||||
payjoinURIs.remove(address);
|
||||
}
|
||||
|
||||
public static void clearTransactionHistoryCache(Wallet wallet) {
|
||||
ElectrumServer.clearRetrievedScriptHashes(wallet);
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isNested()) {
|
||||
AppServices.clearTransactionHistoryCache(childWallet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isWalletFile(File file) {
|
||||
@@ -633,42 +740,25 @@ public class AppServices {
|
||||
return showAlertDialog(title, content == null ? "See log file (Help menu)" : content, Alert.AlertType.ERROR, buttons);
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showSuccessDialog(String title, String content, ButtonType... buttons) {
|
||||
Glyph successGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||
successGlyph.getStyleClass().add("success");
|
||||
successGlyph.setFontSize(50);
|
||||
|
||||
return showAlertDialog(title, content, Alert.AlertType.INFORMATION, successGlyph, buttons);
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {
|
||||
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);
|
||||
return showAlertDialog(title, content, alertType, null, buttons);
|
||||
}
|
||||
|
||||
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) {
|
||||
alert.getDialogPane().setPrefHeight(200 + lines.length * 20);
|
||||
}
|
||||
|
||||
moveToActiveWindowScreen(alert);
|
||||
return alert.showAndWait();
|
||||
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, Node graphic, ButtonType... buttons) {
|
||||
return getInteractionServices().showAlert(title, content, alertType, graphic, buttons);
|
||||
}
|
||||
|
||||
public static void setStageIcon(Window window) {
|
||||
Stage stage = (Stage)window;
|
||||
stage.getIcons().add(new Image(AppServices.class.getResourceAsStream("/image/sparrow-large.png")));
|
||||
stage.getIcons().add(getWindowIcon());
|
||||
|
||||
if(stage.getScene() != null && Config.get().getTheme() == Theme.DARK) {
|
||||
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||
@@ -768,8 +858,10 @@ public class AppServices {
|
||||
Platform.runLater(() -> {
|
||||
if("bitcoin".equals(uri.getScheme())) {
|
||||
openBitcoinUri(uri);
|
||||
} else if("aopp".equals(uri.getScheme())) {
|
||||
openAddressOwnershipProof(uri);
|
||||
} else if(("auth47").equals(uri.getScheme())) {
|
||||
openAuth47Uri(uri);
|
||||
} else if(("lightning").equals(uri.getScheme())) {
|
||||
openLnurlAuthUri(uri);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -792,11 +884,13 @@ public class AppServices {
|
||||
private static void openBitcoinUri(URI uri) {
|
||||
try {
|
||||
BitcoinURI bitcoinURI = new BitcoinURI(uri.toString());
|
||||
Wallet wallet = selectWallet(null, "pay from");
|
||||
List<PolicyType> policyTypes = Arrays.asList(PolicyType.values());
|
||||
List<ScriptType> scriptTypes = Arrays.asList(ScriptType.ADDRESSABLE_TYPES);
|
||||
Wallet wallet = selectWallet(policyTypes, scriptTypes, true, false, "pay from", false);
|
||||
|
||||
if(wallet != null) {
|
||||
final Wallet sendingWallet = wallet;
|
||||
EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getWalletUtxos().keySet())));
|
||||
EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getWalletUtxos().keySet()), true));
|
||||
Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment()))));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
@@ -804,26 +898,97 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
public static void openAddressOwnershipProof(URI uri) {
|
||||
private static void openAuth47Uri(URI uri) {
|
||||
try {
|
||||
Aopp aopp = new Aopp(uri);
|
||||
Wallet wallet = selectWallet(aopp.getScriptType(), "send proof of address");
|
||||
Auth47 auth47 = new Auth47(uri);
|
||||
List<ScriptType> scriptTypes = PaymentCode.SEGWIT_SCRIPT_TYPES;
|
||||
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
|
||||
|
||||
if(wallet != null) {
|
||||
EventManager.get().post(new ReceiveActionEvent(wallet));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveProofEvent(wallet, aopp)));
|
||||
try {
|
||||
auth47.sendResponse(wallet);
|
||||
EventManager.get().post(new StatusEvent("Successfully authenticated to " + auth47.getCallback().getHost()));
|
||||
} catch(Exception e) {
|
||||
log.error("Error authenticating auth47 URI", e);
|
||||
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
showErrorDialog("Not a valid AOPP URI", e.getMessage());
|
||||
log.error("Not a valid auth47 URI", e);
|
||||
showErrorDialog("Not a valid auth47 URI", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Wallet selectWallet(ScriptType scriptType, String actionDescription) {
|
||||
private static void openLnurlAuthUri(URI uri) {
|
||||
try {
|
||||
LnurlAuth lnurlAuth = new LnurlAuth(uri);
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
|
||||
|
||||
if(wallet != null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
copy.decrypt(key);
|
||||
try {
|
||||
lnurlAuth.sendResponse(copy);
|
||||
EventManager.get().post(new StatusEvent("Successfully authenticated to " + lnurlAuth.getDomain()));
|
||||
} catch(Exception e) {
|
||||
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||
} finally {
|
||||
key.clear();
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(() -> openLnurlAuthUri(uri));
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
lnurlAuth.sendResponse(wallet);
|
||||
EventManager.get().post(new StatusEvent("Successfully authenticated to " + lnurlAuth.getDomain()));
|
||||
} catch(LnurlAuth.LnurlAuthException e) {
|
||||
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||
} catch(Exception e) {
|
||||
log.error("Failed to authenticate using LNURL-auth", e);
|
||||
showErrorDialog("Error authenticating", "Failed to authenticate.\n\n" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Not a valid LNURL-auth URI", e);
|
||||
showErrorDialog("Not a valid LNURL-auth URI", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static Wallet selectWallet(List<PolicyType> policyTypes, List<ScriptType> scriptTypes, boolean taprootAllowed, boolean privateKeysRequired, String actionDescription, boolean alwaysAsk) {
|
||||
Wallet wallet = null;
|
||||
List<Wallet> wallets = get().getOpenWallets().keySet().stream().filter(w -> scriptType == null || w.getScriptType() == scriptType).collect(Collectors.toList());
|
||||
List<Wallet> wallets = get().getOpenWallets().keySet().stream().filter(w -> w.isValid() && policyTypes.contains(w.getPolicyType()) && scriptTypes.contains(w.getScriptType())
|
||||
&& (!privateKeysRequired || w.getKeystores().stream().allMatch(Keystore::hasPrivateKey))).collect(Collectors.toList());
|
||||
if(wallets.isEmpty()) {
|
||||
showErrorDialog("No wallet available", "Open a" + (scriptType == null ? "" : " " + scriptType.getDescription()) + " wallet to " + actionDescription + ".");
|
||||
} else if(wallets.size() == 1) {
|
||||
boolean taprootOpen = get().getOpenWallets().keySet().stream().anyMatch(w -> w.getScriptType() == ScriptType.P2TR);
|
||||
showErrorDialog("No wallet available", "Open a" + (taprootOpen && !taprootAllowed ? " non-Taproot" : "") + (privateKeysRequired ? " software" : "") + " wallet to " + actionDescription + ".");
|
||||
} else if(wallets.size() == 1 && !alwaysAsk) {
|
||||
wallet = wallets.iterator().next();
|
||||
} else {
|
||||
ChoiceDialog<Wallet> walletChoiceDialog = new ChoiceDialog<>(wallets.iterator().next(), wallets);
|
||||
@@ -852,7 +1017,7 @@ public class AppServices {
|
||||
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
Config.get().addRecentServer();
|
||||
}
|
||||
@@ -939,7 +1104,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))) {
|
||||
usbWallet = true;
|
||||
|
||||
if(deviceEnumerateService == null) {
|
||||
@@ -966,12 +1131,18 @@ 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
|
||||
connectionService.cancel();
|
||||
ratesService.cancel();
|
||||
versionCheckService.cancel();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -981,11 +1152,27 @@ public class AppServices {
|
||||
|
||||
@Subscribe
|
||||
public void walletOpening(WalletOpeningEvent event) {
|
||||
restartBwt(event.getWallet());
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
Platform.runLater(() -> restartBwt(event.getWallet()));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void childWalletsAdded(ChildWalletsAddedEvent event) {
|
||||
if(event.getChildWallets().stream().anyMatch(Wallet::isNested)) {
|
||||
restartBwt(event.getWallet());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && event.getNestedHistoryChangedNodes().stream().anyMatch(node -> node.getTransactionOutputs().isEmpty())) {
|
||||
Platform.runLater(() -> restartBwt(event.getWallet()));
|
||||
}
|
||||
}
|
||||
|
||||
private void restartBwt(Wallet wallet) {
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && isConnected() && wallet.isValid()) {
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && connectionService != null && connectionService.isConnectionRunning() && wallet.isValid()) {
|
||||
connectionService.cancel();
|
||||
}
|
||||
}
|
||||
@@ -1002,7 +1189,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);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ import com.beust.jcommander.Parameter;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import org.slf4j.event.Level;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Args {
|
||||
@Parameter(names = { "--dir", "-d" }, description = "Path to Sparrow home folder")
|
||||
public String dir;
|
||||
@@ -14,6 +17,31 @@ 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;
|
||||
|
||||
public List<String> toParams() {
|
||||
List<String> params = new ArrayList<>();
|
||||
|
||||
if(dir != null) {
|
||||
params.add("-d");
|
||||
params.add(dir);
|
||||
}
|
||||
if(network != null) {
|
||||
params.add("-n");
|
||||
params.add(network.toString());
|
||||
}
|
||||
if(level != null) {
|
||||
params.add("-l");
|
||||
params.add(level.toString());
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+7
-115
@@ -1,7 +1,5 @@
|
||||
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.glyphfont.FontAwesome5;
|
||||
@@ -12,40 +10,27 @@ 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.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.5.3";
|
||||
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();
|
||||
@@ -77,7 +62,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 +114,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()).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()));
|
||||
} 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.0";
|
||||
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");
|
||||
@@ -0,0 +1,86 @@
|
||||
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", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
|
||||
public DecimalFormat getBtcFormat() {
|
||||
btcFormat.setMaximumFractionDigits(8);
|
||||
return btcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getTableBtcFormat() {
|
||||
return tableBtcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getCurrencyFormat() {
|
||||
return currencyFormat;
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return Locale.ENGLISH;
|
||||
}
|
||||
},
|
||||
COMMA {
|
||||
private final DecimalFormat btcFormat = new DecimalFormat("0", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(getLocale()));
|
||||
|
||||
public DecimalFormat getBtcFormat() {
|
||||
btcFormat.setMaximumFractionDigits(8);
|
||||
return btcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getTableBtcFormat() {
|
||||
return tableBtcFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getCurrencyFormat() {
|
||||
return currencyFormat;
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return Locale.GERMAN;
|
||||
}
|
||||
};
|
||||
|
||||
public abstract Locale getLocale();
|
||||
|
||||
public abstract DecimalFormat getBtcFormat();
|
||||
|
||||
public abstract DecimalFormat getTableBtcFormat();
|
||||
|
||||
public abstract DecimalFormat getCurrencyFormat();
|
||||
|
||||
public String formatBtcValue(Long amount) {
|
||||
return getBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
}
|
||||
|
||||
public String formatSatsValue(Long amount) {
|
||||
return String.format(getLocale(), "%,d", amount);
|
||||
}
|
||||
|
||||
public String formatCurrencyValue(double amount) {
|
||||
return getCurrencyFormat().format(amount);
|
||||
}
|
||||
|
||||
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||
return DecimalFormatSymbols.getInstance(getLocale());
|
||||
}
|
||||
|
||||
public String getGroupingSeparator() {
|
||||
return Character.toString(getDecimalFormatSymbols().getGroupingSeparator());
|
||||
}
|
||||
|
||||
public String getDecimalSeparator() {
|
||||
return Character.toString(getDecimalFormatSymbols().getDecimalSeparator());
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,9 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
existingIndexes.add(masterWallet.getAccountIndex());
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
existingIndexes.add(childWallet.getAccountIndex());
|
||||
if(!childWallet.isNested()) {
|
||||
existingIndexes.add(childWallet.getAccountIndex());
|
||||
}
|
||||
}
|
||||
|
||||
List<StandardAccount> availableAccounts = new ArrayList<>();
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.wallet.Status;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletUtxoStatusChangedEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
public AddressCell() {
|
||||
super();
|
||||
@@ -35,10 +42,13 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
setText(address.toString());
|
||||
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
|
||||
setTooltip(tooltip);
|
||||
|
||||
if(addressStatus.isDuplicate()) {
|
||||
if(addressStatus.isDustAttack()) {
|
||||
setGraphic(getDustAttackHyperlink(utxoEntry));
|
||||
} else if(addressStatus.isDuplicate()) {
|
||||
setGraphic(getDuplicateGlyph());
|
||||
} else {
|
||||
setGraphic(null);
|
||||
@@ -47,8 +57,9 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
}
|
||||
}
|
||||
|
||||
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
|
||||
return utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : "");
|
||||
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate, boolean dustAttack) {
|
||||
return (utxoEntry.getNode().getWallet().isNested() ? utxoEntry.getNode().getWallet().getDisplayName() + " " : "" ) +
|
||||
utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : (dustAttack ? " (Possible dust attack)" : ""));
|
||||
}
|
||||
|
||||
public static Glyph getDuplicateGlyph() {
|
||||
@@ -57,4 +68,22 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
duplicateGlyph.setFontSize(12);
|
||||
return duplicateGlyph;
|
||||
}
|
||||
|
||||
public static Hyperlink getDustAttackHyperlink(UtxoEntry utxoEntry) {
|
||||
Glyph dustAttackGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
dustAttackGlyph.getStyleClass().add("dust-attack-warning");
|
||||
dustAttackGlyph.setFontSize(12);
|
||||
|
||||
Hyperlink hyperlink = new Hyperlink(utxoEntry.getHashIndex().getStatus() == Status.FROZEN ? "" : "Freeze?", dustAttackGlyph);
|
||||
hyperlink.getStyleClass().add("freeze-dust-utxo");
|
||||
hyperlink.setOnAction(event -> {
|
||||
if(utxoEntry.getHashIndex().getStatus() != Status.FROZEN) {
|
||||
hyperlink.setText("");
|
||||
utxoEntry.getHashIndex().setStatus(Status.FROZEN);
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(utxoEntry.getWallet(), Collections.singletonList(utxoEntry.getHashIndex())));
|
||||
}
|
||||
});
|
||||
|
||||
return hyperlink;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.ReceiveActionEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReceiveToEvent;
|
||||
import com.sparrowwallet.sparrow.event.ShowTransactionsCountEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||
import javafx.application.Platform;
|
||||
@@ -13,14 +15,12 @@ import javafx.collections.ListChangeListener;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.MouseButton;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalInt;
|
||||
import java.util.*;
|
||||
|
||||
public class AddressTreeTable extends CoinTreeTable {
|
||||
public void initialize(NodeEntry rootEntry) {
|
||||
getStyleClass().add("address-treetable");
|
||||
setBitcoinUnit(rootEntry.getWallet());
|
||||
setUnitFormat(rootEntry.getWallet());
|
||||
|
||||
String address = rootEntry.getAddress().toString();
|
||||
updateAll(rootEntry);
|
||||
@@ -46,6 +46,15 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
labelCol.setSortable(false);
|
||||
getColumns().add(labelCol);
|
||||
|
||||
TreeTableColumn<Entry, Number> countCol = new TreeTableColumn<>("Transactions");
|
||||
countCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getChildren().size());
|
||||
});
|
||||
countCol.setCellFactory(p -> new NumberCell());
|
||||
countCol.setSortable(false);
|
||||
countCol.setVisible(Config.get().isShowAddressTransactionCount());
|
||||
getColumns().add(countCol);
|
||||
|
||||
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
|
||||
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
|
||||
@@ -54,6 +63,19 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
amountCol.setSortable(false);
|
||||
getColumns().add(amountCol);
|
||||
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
CheckMenuItem showCountItem = new CheckMenuItem("Show Transaction Count");
|
||||
contextMenu.setOnShowing(event -> {
|
||||
showCountItem.setSelected(Config.get().isShowAddressTransactionCount());
|
||||
});
|
||||
showCountItem.setOnAction(event -> {
|
||||
boolean show = !Config.get().isShowAddressTransactionCount();
|
||||
Config.get().setShowAddressTransactionCount(show);
|
||||
EventManager.get().post(new ShowTransactionsCountEvent(show));
|
||||
});
|
||||
contextMenu.getItems().add(showCountItem);
|
||||
getColumns().forEach(col -> col.setContextMenu(contextMenu));
|
||||
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
|
||||
@@ -92,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);
|
||||
@@ -106,17 +128,38 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
}
|
||||
|
||||
public void updateHistory(List<WalletNode> updatedNodes) {
|
||||
//We only ever add child nodes - never remove in order to keep a full sequence
|
||||
//We only ever add child nodes - never remove in order to keep a full sequence (unless hide empty used addresses is set)
|
||||
NodeEntry rootEntry = (NodeEntry)getRoot().getValue();
|
||||
|
||||
Map<WalletNode, NodeEntry> childNodes = new HashMap<>();
|
||||
for(Entry childEntry : rootEntry.getChildren()) {
|
||||
NodeEntry nodeEntry = (NodeEntry)childEntry;
|
||||
childNodes.put(nodeEntry.getNode(), nodeEntry);
|
||||
}
|
||||
|
||||
for(WalletNode updatedNode : updatedNodes) {
|
||||
Optional<Entry> optEntry = rootEntry.getChildren().stream().filter(childEntry -> ((NodeEntry)childEntry).getNode().equals(updatedNode)).findFirst();
|
||||
if(optEntry.isPresent()) {
|
||||
NodeEntry existingEntry = (NodeEntry)optEntry.get();
|
||||
NodeEntry existingEntry = childNodes.get(updatedNode);
|
||||
if(existingEntry != null) {
|
||||
existingEntry.refreshChildren();
|
||||
|
||||
if(Config.get().isHideEmptyUsedAddresses() && existingEntry.getValue() == 0L) {
|
||||
rootEntry.getChildren().remove(existingEntry);
|
||||
}
|
||||
} else {
|
||||
NodeEntry nodeEntry = new NodeEntry(rootEntry.getWallet(), updatedNode);
|
||||
rootEntry.getChildren().add(nodeEntry);
|
||||
|
||||
if(Config.get().isHideEmptyUsedAddresses()) {
|
||||
int index = 0;
|
||||
for( ; index < rootEntry.getChildren().size(); index++) {
|
||||
existingEntry = (NodeEntry)rootEntry.getChildren().get(index);
|
||||
if(nodeEntry.compareTo(existingEntry) < 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
rootEntry.getChildren().add(index, nodeEntry);
|
||||
} else {
|
||||
rootEntry.getChildren().add(nodeEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,4 +170,8 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
Entry rootEntry = getRoot().getValue();
|
||||
rootEntry.updateLabel(entry);
|
||||
}
|
||||
|
||||
public void showTransactionsCount(boolean show) {
|
||||
getColumns().stream().filter(col -> col.getText().equals("Transactions")).forEach(col -> col.setVisible(show));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
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;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class BalanceChart extends LineChart<Number, Number> {
|
||||
private static final int MAX_VALUES = 500;
|
||||
|
||||
private XYChart.Series<Number, Number> balanceSeries;
|
||||
|
||||
private TransactionEntry selectedEntry;
|
||||
@@ -29,15 +36,14 @@ 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) {
|
||||
setVisible(!walletTransactionsEntry.getChildren().isEmpty());
|
||||
balanceSeries.getData().clear();
|
||||
|
||||
List<Data<Number, Number>> balanceDataList = walletTransactionsEntry.getChildren().stream()
|
||||
List<Data<Number, Number>> balanceDataList = getTransactionEntries(walletTransactionsEntry)
|
||||
.map(entry -> (TransactionEntry)entry)
|
||||
.filter(txEntry -> txEntry.getBlockTransaction().getHeight() > 0)
|
||||
.map(txEntry -> new XYChart.Data<>((Number)txEntry.getBlockTransaction().getDate().getTime(), (Number)txEntry.getBalance(), txEntry))
|
||||
@@ -74,6 +80,24 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<Entry> getTransactionEntries(WalletTransactionsEntry walletTransactionsEntry) {
|
||||
int total = walletTransactionsEntry.getChildren().size();
|
||||
if(walletTransactionsEntry.getChildren().size() <= MAX_VALUES) {
|
||||
return walletTransactionsEntry.getChildren().stream();
|
||||
}
|
||||
|
||||
int bucketSize = total / MAX_VALUES;
|
||||
List<List<Entry>> buckets = Lists.partition(walletTransactionsEntry.getChildren(), bucketSize);
|
||||
List<Entry> reducedEntries = new ArrayList<>(MAX_VALUES);
|
||||
for(List<Entry> bucket : buckets) {
|
||||
long max = bucket.stream().mapToLong(entry -> Math.abs(entry.getValue())).max().orElse(0);
|
||||
Entry bucketEntry = bucket.stream().filter(entry -> entry.getValue() == max || entry.getValue() == -max).findFirst().orElseThrow();
|
||||
reducedEntries.add(bucketEntry);
|
||||
}
|
||||
|
||||
return reducedEntries.stream();
|
||||
}
|
||||
|
||||
public void select(TransactionEntry transactionEntry) {
|
||||
Set<Node> selectedSymbols = lookupAll(".chart-line-symbol.selected");
|
||||
for(Node selectedSymbol : selectedSymbols) {
|
||||
@@ -92,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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.chart.Axis;
|
||||
@@ -28,7 +28,7 @@ public class BlockTargetFeeRatesChart extends LineChart<String, Number> {
|
||||
|
||||
for(Iterator<Integer> targetBlocksIter = targetBlocksFeeRates.keySet().iterator(); targetBlocksIter.hasNext(); ) {
|
||||
Integer targetBlocks = targetBlocksIter.next();
|
||||
if(SendController.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
|
||||
if(AppServices.TARGET_BLOCKS_RANGE.contains(targetBlocks)) {
|
||||
String category = targetBlocks + (targetBlocksIter.hasNext() ? "" : "+");
|
||||
XYChart.Data<String, Number> data = new XYChart.Data<>(category, targetBlocksFeeRates.get(targetBlocks));
|
||||
feeRateSeries.getData().add(data);
|
||||
|
||||
@@ -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,6 +2,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.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
|
||||
@@ -10,20 +11,18 @@ import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
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));
|
||||
|
||||
private final Tooltip tooltip;
|
||||
|
||||
public CoinCell() {
|
||||
super();
|
||||
tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(500));
|
||||
getStyleClass().add("coin-cell");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
getStyleClass().add("number-field");
|
||||
@@ -43,10 +42,11 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
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)) {
|
||||
@@ -59,8 +59,7 @@ class CoinCell extends TreeTableCell<Entry, Number> {
|
||||
setTooltip(tooltip);
|
||||
String tooltipValue = tooltip.getText();
|
||||
|
||||
if(entry instanceof TransactionEntry) {
|
||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
tooltip.setText(tooltipValue + " (" + transactionEntry.getConfirmationsDescription() + ")");
|
||||
|
||||
transactionEntry.confirmationsProperty().addListener((observable, oldValue, newValue) -> {
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
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;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
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 CopyableLabel {
|
||||
public static final DecimalFormat BTC_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.ENGLISH));
|
||||
|
||||
public class CoinLabel extends Label {
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
private final CoinContextMenu contextMenu;
|
||||
@@ -28,7 +23,6 @@ public class CoinLabel extends CopyableLabel {
|
||||
|
||||
public CoinLabel(String text) {
|
||||
super(text);
|
||||
BTC_FORMAT.setMaximumFractionDigits(8);
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getBitcoinUnit()));
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new CoinContextMenu();
|
||||
@@ -58,8 +52,9 @@ public class CoinLabel extends CopyableLabel {
|
||||
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)) {
|
||||
@@ -77,7 +72,7 @@ public class CoinLabel extends CopyableLabel {
|
||||
|
||||
private class CoinContextMenu extends ContextMenu {
|
||||
public CoinContextMenu() {
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in Satoshis");
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||
copySatsValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
@@ -89,16 +84,12 @@ public class CoinLabel extends CopyableLabel {
|
||||
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("###,###.########", DecimalFormatSymbols.getInstance(unitFormat.getLocale()));
|
||||
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,10 +2,11 @@ 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;
|
||||
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
@@ -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()) {
|
||||
@@ -86,13 +98,12 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
if(optDate.isPresent()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Wallet pastWallet = wallet.copy();
|
||||
storage.backupTempWallet();
|
||||
wallet.setBirthDate(optDate.get());
|
||||
//Trigger background save of birthdate
|
||||
EventManager.get().post(new WalletDataChangedEvent(wallet));
|
||||
//Trigger full wallet rescan
|
||||
wallet.clearHistory();
|
||||
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
|
||||
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
|
||||
}
|
||||
});
|
||||
if(wallet.getBirthDate() == null) {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
public class CopyableCoinLabel extends CopyableLabel {
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
private final CoinContextMenu contextMenu;
|
||||
|
||||
public CopyableCoinLabel() {
|
||||
this("Unknown");
|
||||
}
|
||||
|
||||
public CopyableCoinLabel(String text) {
|
||||
super(text);
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new CoinContextMenu();
|
||||
}
|
||||
|
||||
public final LongProperty valueProperty() {
|
||||
return valueProperty;
|
||||
}
|
||||
|
||||
public final long getValue() {
|
||||
return valueProperty.get();
|
||||
}
|
||||
|
||||
public final void setValue(long value) {
|
||||
this.valueProperty.set(value);
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
refresh(Config.get().getUnitFormat(), Config.get().getBitcoinUnit());
|
||||
}
|
||||
|
||||
public void refresh(UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
|
||||
setValueAsText(getValue(), unitFormat, bitcoinUnit);
|
||||
}
|
||||
|
||||
private void setValueAsText(Long value, UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
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)) {
|
||||
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
|
||||
}
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setText(satsValue);
|
||||
setText(btcValue);
|
||||
} else {
|
||||
tooltip.setText(btcValue);
|
||||
setText(satsValue);
|
||||
}
|
||||
}
|
||||
|
||||
private class CoinContextMenu extends ContextMenu {
|
||||
public CoinContextMenu() {
|
||||
MenuItem copySatsValue = new MenuItem("Copy Value in sats");
|
||||
copySatsValue.setOnAction(AE -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(Long.toString(getValue()));
|
||||
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(getValue()));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(copySatsValue, copyBtcValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
@@ -46,6 +47,7 @@ public class DateCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
int height = utxoEntry.getHashIndex().getHeight();
|
||||
tooltip.setText(height > 0 ? Integer.toString(height) : "Mempool");
|
||||
setTooltip(tooltip);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
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;
|
||||
|
||||
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
|
||||
super(ur);
|
||||
|
||||
DialogPane dialogPane = getDialogPane();
|
||||
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
|
||||
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 -> {
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur);
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
@@ -697,17 +696,17 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallet, importedKeystores);
|
||||
walletDiscoveryService.setOnSucceeded(event -> {
|
||||
importedKeystores.keySet().retainAll(walletDiscoveryService.getValue());
|
||||
ElectrumServer.AccountDiscoveryService accountDiscoveryService = new ElectrumServer.AccountDiscoveryService(wallet, importedKeystores);
|
||||
accountDiscoveryService.setOnSucceeded(event -> {
|
||||
importedKeystores.keySet().retainAll(accountDiscoveryService.getValue());
|
||||
EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores));
|
||||
});
|
||||
walletDiscoveryService.setOnFailed(event -> {
|
||||
accountDiscoveryService.setOnFailed(event -> {
|
||||
log.error("Failed to discover accounts", event.getSource().getException());
|
||||
setError("Failed to discover accounts", event.getSource().getException().getMessage());
|
||||
discoverKeystoresButton.setDisable(false);
|
||||
});
|
||||
walletDiscoveryService.start();
|
||||
accountDiscoveryService.start();
|
||||
});
|
||||
getXpubsService.setOnFailed(workerStateEvent -> {
|
||||
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
|
||||
|
||||
@@ -3,15 +3,13 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
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.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -19,6 +17,7 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.FontAwesome;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
@@ -37,6 +36,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
public static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
|
||||
private static final Pattern REPLACED_BY_FEE_SUFFIX = Pattern.compile("(.*)\\(Replaced By Fee( #)?(\\d+)?\\).*");
|
||||
|
||||
private static EntryCell lastCell;
|
||||
|
||||
public EntryCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
@@ -48,6 +49,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
protected void updateItem(Entry entry, boolean empty) {
|
||||
super.updateItem(entry, empty);
|
||||
|
||||
//Return immediately to avoid CPU usage when updating the same invisible cell to determine tableview size (see https://bugs.openjdk.org/browse/JDK-8280442)
|
||||
if(this == lastCell && !getTableRow().isVisible()) {
|
||||
return;
|
||||
}
|
||||
lastCell = this;
|
||||
|
||||
applyRowStyles(this, entry);
|
||||
|
||||
if(empty) {
|
||||
@@ -69,10 +76,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
}
|
||||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setText(transactionEntry.getBlockTransaction().getHash().toString());
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(getTooltip(transactionEntry));
|
||||
setTooltip(tooltip);
|
||||
|
||||
if(transactionEntry.getBlockTransaction().getHeight() <= 0) {
|
||||
tooltip.setOnShowing(event -> {
|
||||
tooltip.setText(getTooltip(transactionEntry));
|
||||
});
|
||||
}
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
Button viewTransactionButton = new Button("");
|
||||
viewTransactionButton.setGraphic(getViewTransactionGlyph());
|
||||
viewTransactionButton.setOnAction(event -> {
|
||||
@@ -81,11 +96,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);
|
||||
}
|
||||
@@ -106,20 +122,25 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
setText(address.toString());
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(nodeEntry.getNode().toString());
|
||||
setTooltip(tooltip);
|
||||
getStyleClass().add("address-cell");
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
Button receiveButton = new Button("");
|
||||
receiveButton.setGraphic(getReceiveGlyph());
|
||||
receiveButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
actionBox.getChildren().add(receiveButton);
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
|
||||
if(canSignMessage(nodeEntry.getWallet())) {
|
||||
if(!nodeEntry.getNode().getWallet().isBip47()) {
|
||||
Button receiveButton = new Button("");
|
||||
receiveButton.setGraphic(getReceiveGlyph());
|
||||
receiveButton.setOnAction(event -> {
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
actionBox.getChildren().add(receiveButton);
|
||||
}
|
||||
|
||||
if(canSignMessage(nodeEntry.getNode())) {
|
||||
Button signMessageButton = new Button("");
|
||||
signMessageButton.setGraphic(getSignMessageGlyph());
|
||||
signMessageButton.setOnAction(event -> {
|
||||
@@ -139,12 +160,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||
setText(hashIndexEntry.getDescription());
|
||||
setContextMenu(new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(hashIndexEntry.getHashIndex().toString());
|
||||
setTooltip(tooltip);
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
Button viewTransactionButton = new Button("");
|
||||
viewTransactionButton.setGraphic(getViewTransactionGlyph());
|
||||
viewTransactionButton.setOnAction(event -> {
|
||||
@@ -161,12 +184,12 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
actionBox.getChildren().add(spendUtxoButton);
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
setGraphic(getTreeTableView().getStyleClass().contains("bip47") ? null : actionBox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void increaseFee(TransactionEntry transactionEntry) {
|
||||
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()
|
||||
@@ -178,6 +201,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)
|
||||
@@ -192,14 +221,16 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||
Transaction tx = blockTransaction.getTransaction();
|
||||
double vSize = tx.getVirtualSize();
|
||||
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
|
||||
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
|
||||
//Remove any UTXOs created by the transaction that is to be replaced
|
||||
walletUtxos.removeIf(utxo -> ourOutputs.stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex()));
|
||||
Collections.shuffle(walletUtxos);
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty()) {
|
||||
//If there is insufficent change output, include another random UTXO so the fee can be increased
|
||||
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);
|
||||
changeTotal += utxo.getValue();
|
||||
@@ -209,6 +240,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
List<TransactionOutput> externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs());
|
||||
externalOutputs.removeAll(ourOutputs);
|
||||
externalOutputs.addAll(consolidationOutputs);
|
||||
final long rbfChange = changeTotal;
|
||||
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
|
||||
try {
|
||||
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
||||
@@ -225,20 +257,46 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
label += (label.isEmpty() ? "" : " ") + "(Replaced By Fee)";
|
||||
}
|
||||
|
||||
return new Payment(txOutput.getScript().getToAddresses()[0], label, txOutput.getValue(), false);
|
||||
if(txOutput.getScript().getToAddress() != null) {
|
||||
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
|
||||
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating RBF payment", e);
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
List<byte[]> opReturns = externalOutputs.stream().map(txOutput -> {
|
||||
List<ScriptChunk> scriptChunks = txOutput.getScript().getChunks();
|
||||
if(scriptChunks.size() != 2 || scriptChunks.get(0).getOpcode() != ScriptOpCodes.OP_RETURN) {
|
||||
return null;
|
||||
}
|
||||
if(scriptChunks.get(1).getData() != null) {
|
||||
return scriptChunks.get(1).getData();
|
||||
}
|
||||
|
||||
return null;
|
||||
}).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
if(payments.isEmpty()) {
|
||||
AppServices.showErrorDialog("Replace By Fee Error", "Error creating RBF transaction, check log for details");
|
||||
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, blockTransaction.getFee(), true)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, blockTransaction.getFee(), true)));
|
||||
}
|
||||
|
||||
private static Double getMaxFeeRate() {
|
||||
@@ -267,15 +325,17 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
WalletNode freshNode = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE);
|
||||
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
||||
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
|
||||
Payment payment = new Payment(transactionEntry.getWallet().getAddress(freshNode), label, utxo.getValue(), true);
|
||||
Payment payment = new Payment(freshNode.getAddress(), label, utxo.getValue(), true);
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), List.of(utxo)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), blockTransaction.getFee(), false)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), List.of(utxo), List.of(payment), null, blockTransaction.getFee(), false)));
|
||||
}
|
||||
|
||||
private static boolean canSignMessage(Wallet wallet) {
|
||||
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.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||
@@ -326,6 +386,34 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
EventManager.get().post(new WalletUtxoStatusChangedEvent(hashIndexEntry.getWallet(), utxos));
|
||||
}
|
||||
|
||||
private String getTooltip(TransactionEntry transactionEntry) {
|
||||
String tooltip = transactionEntry.getBlockTransaction().getHash().toString();
|
||||
if(transactionEntry.getBlockTransaction().getHeight() <= 0) {
|
||||
Double feeRate = transactionEntry.getBlockTransaction().getFeeRate();
|
||||
Long vSizefromTip = transactionEntry.getVSizeFromTip();
|
||||
if(feeRate != null && vSizefromTip != null) {
|
||||
long blocksFromTip = (long)Math.ceil((double)vSizefromTip / Transaction.MAX_BLOCK_SIZE);
|
||||
|
||||
String amount = vSizefromTip + " vB";
|
||||
if(vSizefromTip > 1000 * 1000) {
|
||||
amount = String.format("%.2f", (double)vSizefromTip / (1000 * 1000)) + " MvB";
|
||||
} else if(vSizefromTip > 1000) {
|
||||
amount = String.format("%.2f", (double)vSizefromTip / 1000) + " kvB";
|
||||
}
|
||||
|
||||
tooltip += "\nConfirms in: " + (blocksFromTip > 1 ? blocksFromTip + "+ blocks" : "1 block") + " (" + amount + " from tip)";
|
||||
}
|
||||
|
||||
if(feeRate != null) {
|
||||
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
||||
}
|
||||
|
||||
tooltip += "\nRBF: " + (transactionEntry.getBlockTransaction().getTransaction().isReplaceByFee() ? "Enabled" : "Disabled");
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
private static Glyph getViewTransactionGlyph() {
|
||||
Glyph searchGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SEARCH);
|
||||
searchGlyph.setFontSize(12);
|
||||
@@ -338,6 +426,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);
|
||||
@@ -391,17 +485,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());
|
||||
@@ -464,16 +569,18 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
|
||||
public static class AddressContextMenu extends ContextMenu {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
receiveToAddress.setOnAction(event -> {
|
||||
hide();
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
getItems().add(receiveToAddress);
|
||||
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
receiveToAddress.setOnAction(event -> {
|
||||
hide();
|
||||
EventManager.get().post(new ReceiveActionEvent(nodeEntry));
|
||||
Platform.runLater(() -> EventManager.get().post(new ReceiveToEvent(nodeEntry)));
|
||||
});
|
||||
getItems().add(receiveToAddress);
|
||||
}
|
||||
|
||||
if(nodeEntry != null && canSignMessage(nodeEntry.getWallet())) {
|
||||
if(nodeEntry != null && canSignMessage(nodeEntry.getNode())) {
|
||||
MenuItem signVerifyMessage = new MenuItem("Sign/Verify Message");
|
||||
signVerifyMessage.setGraphic(getSignMessageGlyph());
|
||||
signVerifyMessage.setOnAction(AE -> {
|
||||
@@ -484,6 +591,42 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
|
||||
getItems().add(signVerifyMessage);
|
||||
}
|
||||
|
||||
if(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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -42,12 +42,14 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
protected ButtonBase importButton;
|
||||
private final SimpleStringProperty password = new SimpleStringProperty("");
|
||||
private final boolean scannable;
|
||||
private final boolean fileFormatAvailable;
|
||||
protected List<Wallet> wallets;
|
||||
|
||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable) {
|
||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
|
||||
super(title, description, content, imageUrl);
|
||||
this.importer = importer;
|
||||
this.scannable = scannable;
|
||||
this.fileFormatAvailable = fileFormatAvailable;
|
||||
|
||||
buttonBox.getChildren().clear();
|
||||
buttonBox.getChildren().add(createButton());
|
||||
@@ -55,7 +57,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
if(scannable) {
|
||||
if(scannable && fileFormatAvailable) {
|
||||
ToggleButton scanButton = new ToggleButton("Scan...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
@@ -76,6 +78,16 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
SegmentedButton segmentedButton = new SegmentedButton();
|
||||
segmentedButton.getButtons().addAll(scanButton, fileButton);
|
||||
return segmentedButton;
|
||||
} else if(scannable) {
|
||||
importButton = new Button("Scan...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
importButton.setGraphic(cameraGlyph);
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setOnAction(event -> {
|
||||
importQR();
|
||||
});
|
||||
return importButton;
|
||||
} else {
|
||||
importButton = new Button("Import File...");
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -146,6 +158,15 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
try {
|
||||
importFile(importer.getName(), null, null);
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing QR", e);
|
||||
setError("Import Error", e.getMessage());
|
||||
}
|
||||
} else if(result.outputDescriptor != null) {
|
||||
wallets = List.of(result.outputDescriptor.toKeystoreWallet(null));
|
||||
try {
|
||||
importFile(importer.getName(), null, null);
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing QR", e);
|
||||
setError("Import Error", e.getMessage());
|
||||
}
|
||||
} else if(result.payload != null) {
|
||||
@@ -165,6 +186,9 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
} else if(result.exception != null) {
|
||||
log.error("Error importing QR", result.exception);
|
||||
setError("Import Error", result.exception.getMessage());
|
||||
} else {
|
||||
setError("Import Error", null);
|
||||
setExpanded(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
private final KeyDerivation requiredDerivation;
|
||||
|
||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
@@ -34,4 +34,17 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
}
|
||||
|
||||
private static int getAccount(Wallet wallet, KeyDerivation requiredDerivation) {
|
||||
if(wallet == null || requiredDerivation == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int account = wallet.getScriptType().getAccount(requiredDerivation.getDerivationPath());
|
||||
if(account < 0) {
|
||||
account = 0;
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
@@ -80,8 +81,8 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Export " + exporter.getWalletModel().toDisplayString() + " File");
|
||||
String extension = exporter.getExportFileExtension(wallet);
|
||||
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase().replace(" ", "");
|
||||
if(exporter instanceof Sparrow) {
|
||||
String fileName = wallet.getFullName() + "-" + exporter.getWalletModel().toDisplayString().toLowerCase(Locale.ROOT).replace(" ", "");
|
||||
if(exporter.exportsAllWallets()) {
|
||||
fileName = wallet.getMasterName();
|
||||
}
|
||||
fileChooser.setInitialFileName(fileName + (extension == null || extension.isEmpty() ? "" : "." + extension));
|
||||
|
||||
@@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane {
|
||||
private final WalletImport importer;
|
||||
|
||||
public FileWalletImportPane(WalletImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable());
|
||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), true);
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private byte[] fileBytes;
|
||||
|
||||
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ public class HelpLabel extends Label {
|
||||
tooltip.textProperty().bind(helpTextProperty());
|
||||
tooltip.graphicProperty().bind(helpGraphicProperty());
|
||||
tooltip.setShowDuration(Duration.seconds(15));
|
||||
tooltip.setShowDelay(Duration.millis(500));
|
||||
getStyleClass().add("help-label");
|
||||
|
||||
Platform.runLater(() -> setTooltip(tooltip));
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.scene.control.Spinner;
|
||||
import javafx.scene.control.SpinnerValueFactory;
|
||||
import javafx.util.converter.IntegerStringConverter;
|
||||
|
||||
public class IntegerSpinner extends Spinner<Integer> {
|
||||
public IntegerSpinner() {
|
||||
super();
|
||||
setupEditor();
|
||||
}
|
||||
|
||||
public IntegerSpinner(@NamedArg("min") int min,
|
||||
@NamedArg("max") int max,
|
||||
@NamedArg("initialValue") int initialValue) {
|
||||
super(min, max, initialValue);
|
||||
setupEditor();
|
||||
}
|
||||
|
||||
public IntegerSpinner(@NamedArg("min") int min,
|
||||
@NamedArg("max") int max,
|
||||
@NamedArg("initialValue") int initialValue,
|
||||
@NamedArg("amountToStepBy") int amountToStepBy) {
|
||||
super(min, max, initialValue, amountToStepBy);
|
||||
setupEditor();
|
||||
}
|
||||
|
||||
private void setupEditor() {
|
||||
getEditor().focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null && !newValue) {
|
||||
commitValue();
|
||||
}
|
||||
});
|
||||
getEditor().textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(!newValue.matches("\\d*")) {
|
||||
getEditor().setText(newValue.replaceAll("[^\\d]", ""));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static class ValueFactory extends SpinnerValueFactory.IntegerSpinnerValueFactory {
|
||||
public ValueFactory(@NamedArg("min") int min,
|
||||
@NamedArg("max") int max) {
|
||||
super(min, max);
|
||||
setupConverter(min);
|
||||
}
|
||||
|
||||
public ValueFactory(@NamedArg("min") int min,
|
||||
@NamedArg("max") int max,
|
||||
@NamedArg("initialValue") int initialValue) {
|
||||
super(min, max, initialValue);
|
||||
setupConverter(initialValue);
|
||||
}
|
||||
|
||||
public ValueFactory(@NamedArg("min") int min,
|
||||
@NamedArg("max") int max,
|
||||
@NamedArg("initialValue") int initialValue,
|
||||
@NamedArg("amountToStepBy") int amountToStepBy) {
|
||||
super(min, max, initialValue, amountToStepBy);
|
||||
setupConverter(initialValue);
|
||||
}
|
||||
|
||||
private void setupConverter(Integer defaultValue) {
|
||||
setConverter(new IntegerStringConverter() {
|
||||
@Override
|
||||
public Integer fromString(String value) {
|
||||
if(value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
value = value.trim();
|
||||
|
||||
if(value.length() < 1) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return Integer.valueOf(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,18 +92,15 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> {
|
||||
});
|
||||
getItems().add(copyLabel);
|
||||
|
||||
Object content = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
|
||||
if(content instanceof String) {
|
||||
MenuItem pasteLabel = new MenuItem("Paste Label");
|
||||
pasteLabel.setOnAction(AE -> {
|
||||
hide();
|
||||
Object currentContent = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
|
||||
if(currentContent instanceof String) {
|
||||
entry.labelProperty().set((String)currentContent);
|
||||
}
|
||||
});
|
||||
getItems().add(pasteLabel);
|
||||
}
|
||||
MenuItem pasteLabel = new MenuItem("Paste Label");
|
||||
pasteLabel.setOnAction(AE -> {
|
||||
hide();
|
||||
Object currentContent = Clipboard.getSystemClipboard().getContent(DataFormat.PLAIN_TEXT);
|
||||
if(currentContent instanceof String) {
|
||||
entry.labelProperty().set((String)currentContent);
|
||||
}
|
||||
});
|
||||
getItems().add(pasteLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.MempoolRateSize;
|
||||
import com.sparrowwallet.sparrow.wallet.SendController;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -79,9 +79,9 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
});
|
||||
|
||||
long previousFeeRate = 0;
|
||||
for(Long feeRate : SendController.FEE_RATES_RANGE) {
|
||||
for(Long feeRate : AppServices.FEE_RATES_RANGE) {
|
||||
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
||||
series.setName(feeRate + "+ vB");
|
||||
series.setName(feeRate + "+ sats/vB");
|
||||
long seriesTotalVSize = 0;
|
||||
|
||||
for(Date date : periodRateSizes.keySet()) {
|
||||
|
||||
@@ -37,6 +37,7 @@ import tornadofx.control.Form;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
@@ -89,13 +90,12 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
* @param buttons The dialog buttons to display. If one contains the text "sign" it will trigger the signing process
|
||||
*/
|
||||
public MessageSignDialog(Wallet wallet, WalletNode walletNode, String title, String msg, ButtonType... buttons) {
|
||||
if(walletNode != null) {
|
||||
checkWalletSigning(walletNode.getWallet());
|
||||
}
|
||||
|
||||
if(wallet != null) {
|
||||
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 a seed or USB keystore");
|
||||
}
|
||||
checkWalletSigning(wallet);
|
||||
}
|
||||
|
||||
this.wallet = wallet;
|
||||
@@ -103,6 +103,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
|
||||
|
||||
@@ -120,16 +121,18 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
fieldset.setSpacing(10);
|
||||
|
||||
Field addressField = new Field();
|
||||
addressField.setText("Address:");
|
||||
address = new TextField();
|
||||
address.getStyleClass().add("id");
|
||||
address.setEditable(walletNode == null);
|
||||
address.setTooltip(new Tooltip("Only Legacy (P2PKH), Nested Segwit (P2SH-P2WPKH) and Native Segwit (P2WPKH) singlesig addresses can sign"));
|
||||
addressField.getInputs().add(address);
|
||||
|
||||
if(walletNode != null) {
|
||||
address.setText(wallet.getAddress(walletNode).toString());
|
||||
address.setText(walletNode.getAddress().toString());
|
||||
}
|
||||
|
||||
Field messageField = new Field();
|
||||
@@ -183,7 +186,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
if(buttons.length > 0) {
|
||||
dialogPane.getButtonTypes().addAll(buttons);
|
||||
|
||||
ButtonType customSignButtonType = Arrays.stream(buttons).filter(buttonType -> buttonType.getText().toLowerCase().contains("sign")).findFirst().orElse(null);
|
||||
ButtonType customSignButtonType = Arrays.stream(buttons).filter(buttonType -> buttonType.getText().toLowerCase(Locale.ROOT).contains("sign")).findFirst().orElse(null);
|
||||
if(customSignButtonType != null) {
|
||||
Button customSignButton = (Button)dialogPane.lookupButton(customSignButtonType);
|
||||
customSignButton.setDefaultButton(true);
|
||||
@@ -262,6 +265,15 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
});
|
||||
}
|
||||
|
||||
private void checkWalletSigning(Wallet wallet) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
private Address getAddress()throws InvalidAddressException {
|
||||
return Address.fromString(address.getText());
|
||||
}
|
||||
@@ -283,7 +295,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private boolean isValidAddress() {
|
||||
try {
|
||||
Address address = getAddress();
|
||||
return address.getScriptType() != ScriptType.P2TR;
|
||||
return address.getScriptType() != ScriptType.P2TR && address.getScriptType().isAllowed(PolicyType.SINGLE);
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -299,20 +311,21 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
if(wallet.containsPrivateKeys()) {
|
||||
if(wallet.isEncrypted()) {
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
if(signingWallet.getKeystores().get(0).hasPrivateKey()) {
|
||||
if(signingWallet.isEncrypted()) {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
} else {
|
||||
signUnencryptedKeystore(wallet);
|
||||
signUnencryptedKeystore(signingWallet);
|
||||
}
|
||||
} else if(wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
signUsbKeystore(wallet);
|
||||
} else if(signingWallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
signUsbKeystore(signingWallet);
|
||||
}
|
||||
}
|
||||
|
||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||
try {
|
||||
//Note we can expect a single keystore due to the check above
|
||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
ScriptType scriptType = electrumSignatureFormat ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||
@@ -359,15 +372,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
if(verified) {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||
AppServices.setStageIcon(alert.getDialogPane().getScene().getWindow());
|
||||
alert.setTitle("Verification Succeeded");
|
||||
alert.setHeaderText("Verification Succeeded");
|
||||
alert.setContentText("The signature verified against the message.");
|
||||
AppServices.moveToActiveWindowScreen(alert);
|
||||
alert.showAndWait();
|
||||
AppServices.showSuccessDialog("Verification Succeeded", "The signature verified against the message.");
|
||||
} else {
|
||||
AppServices.showErrorDialog("Verification failed", "The provided signature did not match the message for this address.");
|
||||
AppServices.showErrorDialog("Verification Failed", "The provided signature did not match the message for this address.");
|
||||
}
|
||||
} catch(IllegalArgumentException e) {
|
||||
AppServices.showErrorDialog("Could not verify message", e.getMessage());
|
||||
@@ -402,7 +409,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(walletNode.getWallet().copy(), password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
|
||||
@@ -17,6 +17,8 @@ import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
|
||||
|
||||
@@ -114,7 +116,7 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1);
|
||||
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase(Locale.ROOT) + mixProgress.getMixStep().getMessage().substring(1);
|
||||
tt.setText(status);
|
||||
setTooltip(tt);
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
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;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.TilePane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
public MnemonicKeystoreDisplayPane(Keystore keystore) {
|
||||
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
showHideLink.setVisible(false);
|
||||
buttonBox.getChildren().clear();
|
||||
|
||||
showWordList(keystore.getSeed());
|
||||
}
|
||||
|
||||
protected Node getMnemonicWordsEntry(int numWords) {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords / 3);
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
|
||||
List<String> words = new ArrayList<>();
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
words.add("");
|
||||
}
|
||||
|
||||
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
|
||||
}
|
||||
wordsPane.getChildren().addAll(wordEntries);
|
||||
|
||||
vBox.getChildren().add(wordsPane);
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(vBox);
|
||||
return stackPane;
|
||||
}
|
||||
}
|
||||
@@ -3,30 +3,22 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreMnemonicImport;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Callback;
|
||||
import org.controlsfx.control.textfield.AutoCompletionBinding;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.tools.Borders;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
@@ -35,34 +27,24 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected final Wallet wallet;
|
||||
private final KeystoreMnemonicImport importer;
|
||||
|
||||
private SplitMenuButton enterMnemonicButton;
|
||||
private SplitMenuButton importButton;
|
||||
|
||||
private TilePane wordsPane;
|
||||
private Button generateButton;
|
||||
private Label validLabel;
|
||||
private Label invalidLabel;
|
||||
private Button calculateButton;
|
||||
private Button backButton;
|
||||
private Button nextButton;
|
||||
private Button confirmButton;
|
||||
private List<String> generatedMnemonicCode;
|
||||
|
||||
private SimpleListProperty<String> wordEntriesProperty;
|
||||
private final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
|
||||
private IntegerProperty defaultWordSizeProperty;
|
||||
|
||||
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
|
||||
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
|
||||
@@ -70,46 +52,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
buttonBox.getChildren().add(importButton);
|
||||
}
|
||||
|
||||
public MnemonicKeystoreImportPane(Keystore keystore) {
|
||||
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
this.wallet = null;
|
||||
this.importer = null;
|
||||
showHideLink.setVisible(false);
|
||||
buttonBox.getChildren().clear();
|
||||
|
||||
showWordList(keystore.getSeed());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
createEnterMnemonicButton();
|
||||
return enterMnemonicButton;
|
||||
}
|
||||
|
||||
private void createEnterMnemonicButton() {
|
||||
enterMnemonicButton = new SplitMenuButton();
|
||||
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
enterMnemonicButton.setText("Enter 24 Words");
|
||||
defaultWordSizeProperty = new SimpleIntegerProperty(24);
|
||||
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
|
||||
enterMnemonicButton.setText("Enter " + newValue + " Words");
|
||||
});
|
||||
enterMnemonicButton.setOnAction(event -> {
|
||||
enterMnemonic(defaultWordSizeProperty.get());
|
||||
});
|
||||
int[] numberWords = new int[] {24, 21, 18, 15, 12};
|
||||
for(int i = 0; i < numberWords.length; i++) {
|
||||
MenuItem item = new MenuItem("Enter " + numberWords[i] + " Words");
|
||||
final int words = numberWords[i];
|
||||
item.setOnAction(event -> {
|
||||
defaultWordSizeProperty.set(words);
|
||||
enterMnemonic(words);
|
||||
});
|
||||
enterMnemonicButton.getItems().add(item);
|
||||
}
|
||||
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
||||
}
|
||||
|
||||
private void createImportButton() {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -135,162 +77,81 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
importButton.setVisible(false);
|
||||
}
|
||||
|
||||
private void enterMnemonic(int numWords) {
|
||||
protected void enterMnemonic(int numWords) {
|
||||
generatedMnemonicCode = null;
|
||||
setDescription("Enter mnemonic word list");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getMnemonicWordsEntry(numWords, false));
|
||||
setExpanded(true);
|
||||
super.enterMnemonic(numWords);
|
||||
}
|
||||
|
||||
private Node getMnemonicWordsEntry(int numWords, boolean displayWordsOnly) {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
protected List<Node> createLeftButtons() {
|
||||
generateButton = new Button("Generate New");
|
||||
generateButton.setOnAction(event -> {
|
||||
generateNew();
|
||||
});
|
||||
generateButton.managedProperty().bind(generateButton.visibleProperty());
|
||||
generateButton.setTooltip(new Tooltip("Generate a unique set of words that provide the seed for your wallet"));
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords/3);
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
return List.of(generateButton);
|
||||
}
|
||||
|
||||
List<String> words = new ArrayList<>();
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
words.add("");
|
||||
}
|
||||
protected List<Node> createRightButtons() {
|
||||
confirmButton = new Button("Re-enter Words...");
|
||||
confirmButton.setOnAction(event -> {
|
||||
confirmBackup();
|
||||
});
|
||||
confirmButton.managedProperty().bind(confirmButton.visibleProperty());
|
||||
confirmButton.setVisible(false);
|
||||
confirmButton.setDefaultButton(true);
|
||||
confirmButton.setTooltip(new Tooltip("Re-enter the generated word list to confirm your backup is correct"));
|
||||
|
||||
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
|
||||
}
|
||||
wordsPane.getChildren().addAll(wordEntries);
|
||||
calculateButton = new Button("Create Keystore");
|
||||
calculateButton.setDisable(true);
|
||||
calculateButton.setDefaultButton(true);
|
||||
calculateButton.setOnAction(event -> {
|
||||
prepareImport();
|
||||
});
|
||||
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
|
||||
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided word list"));
|
||||
|
||||
vBox.getChildren().add(wordsPane);
|
||||
backButton = new Button("Back");
|
||||
backButton.setOnAction(event -> {
|
||||
displayMnemonicCode();
|
||||
});
|
||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
backButton.setTooltip(new Tooltip("Go back to the generated word list"));
|
||||
backButton.setVisible(false);
|
||||
|
||||
if(!displayWordsOnly) {
|
||||
PassphraseEntry passphraseEntry = new PassphraseEntry();
|
||||
wordEntries.get(wordEntries.size() - 1).setNextField(passphraseEntry.getEditor());
|
||||
passphraseEntry.setPadding(new Insets(0, 26, 10, 10));
|
||||
vBox.getChildren().add(passphraseEntry);
|
||||
nextButton = new Button("Confirm Backup...");
|
||||
nextButton.setOnAction(event -> {
|
||||
confirmRecord();
|
||||
});
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
nextButton.setTooltip(new Tooltip("Confirm you have recorded the generated word list"));
|
||||
nextButton.setVisible(false);
|
||||
nextButton.setDefaultButton(true);
|
||||
|
||||
AnchorPane buttonPane = new AnchorPane();
|
||||
buttonPane.setPadding(new Insets(0, 26, 0, 10));
|
||||
return List.of(backButton, nextButton, confirmButton, calculateButton);
|
||||
}
|
||||
|
||||
generateButton = new Button("Generate New");
|
||||
generateButton.setOnAction(event -> {
|
||||
generateNew();
|
||||
});
|
||||
generateButton.managedProperty().bind(generateButton.visibleProperty());
|
||||
generateButton.setTooltip(new Tooltip("Generate a unique set of words that provide the seed for your wallet"));
|
||||
buttonPane.getChildren().add(generateButton);
|
||||
AnchorPane.setLeftAnchor(generateButton, 0.0);
|
||||
|
||||
validLabel = new Label("Valid checksum", getValidGlyph());
|
||||
validLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
validLabel.setGraphicTextGap(5.0);
|
||||
validLabel.managedProperty().bind(validLabel.visibleProperty());
|
||||
validLabel.setVisible(false);
|
||||
buttonPane.getChildren().add(validLabel);
|
||||
AnchorPane.setTopAnchor(validLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(validLabel, 0.0);
|
||||
|
||||
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
|
||||
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
invalidLabel.setGraphicTextGap(5.0);
|
||||
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
|
||||
invalidLabel.setVisible(false);
|
||||
buttonPane.getChildren().add(invalidLabel);
|
||||
AnchorPane.setTopAnchor(invalidLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
|
||||
|
||||
confirmButton = new Button("Re-enter Words...");
|
||||
confirmButton.setOnAction(event -> {
|
||||
confirmBackup();
|
||||
});
|
||||
confirmButton.managedProperty().bind(confirmButton.visibleProperty());
|
||||
confirmButton.setVisible(false);
|
||||
confirmButton.setDefaultButton(true);
|
||||
confirmButton.setTooltip(new Tooltip("Re-enter the generated word list to confirm your backup is correct"));
|
||||
|
||||
calculateButton = new Button("Create Keystore");
|
||||
calculateButton.setDisable(true);
|
||||
calculateButton.setDefaultButton(true);
|
||||
calculateButton.setOnAction(event -> {
|
||||
prepareImport();
|
||||
});
|
||||
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
|
||||
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided word list"));
|
||||
|
||||
backButton = new Button("Back");
|
||||
backButton.setOnAction(event -> {
|
||||
displayMnemonicCode();
|
||||
});
|
||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
backButton.setTooltip(new Tooltip("Go back to the generated word list"));
|
||||
backButton.setVisible(false);
|
||||
|
||||
nextButton = new Button("Confirm Backup...");
|
||||
nextButton.setOnAction(event -> {
|
||||
confirmRecord();
|
||||
});
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
nextButton.setTooltip(new Tooltip("Confirm you have recorded the generated word list"));
|
||||
nextButton.setVisible(false);
|
||||
nextButton.setDefaultButton(true);
|
||||
|
||||
wordEntriesProperty.addListener((ListChangeListener<String>) c -> {
|
||||
boolean empty = true;
|
||||
boolean validWords = true;
|
||||
boolean validChecksum = false;
|
||||
for(String word : wordEntryList) {
|
||||
if(!word.isEmpty()) {
|
||||
empty = false;
|
||||
}
|
||||
|
||||
if(!WordEntry.isValid(word)) {
|
||||
validWords = false;
|
||||
}
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
|
||||
invalidLabel.setText("Unsupported Electrum seed");
|
||||
invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly."));
|
||||
} else {
|
||||
invalidLabel.setText("Invalid checksum");
|
||||
invalidLabel.setTooltip(null);
|
||||
}
|
||||
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
|
||||
invalidLabel.setText("Unsupported Electrum seed");
|
||||
invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly."));
|
||||
} else {
|
||||
invalidLabel.setText("Invalid checksum");
|
||||
invalidLabel.setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generateButton.setVisible(empty && generatedMnemonicCode == null);
|
||||
calculateButton.setDisable(!validChecksum);
|
||||
validLabel.setVisible(validChecksum);
|
||||
invalidLabel.setVisible(!validChecksum && !empty);
|
||||
});
|
||||
|
||||
HBox rightBox = new HBox();
|
||||
rightBox.setSpacing(10);
|
||||
rightBox.getChildren().addAll(backButton, nextButton, confirmButton, calculateButton);
|
||||
|
||||
buttonPane.getChildren().add(rightBox);
|
||||
AnchorPane.setRightAnchor(rightBox, 0.0);
|
||||
|
||||
vBox.getChildren().add(buttonPane);
|
||||
}
|
||||
}
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(vBox);
|
||||
return stackPane;
|
||||
generateButton.setVisible(empty && generatedMnemonicCode == null);
|
||||
calculateButton.setDisable(!validChecksum);
|
||||
validLabel.setVisible(validChecksum);
|
||||
invalidLabel.setVisible(!validChecksum && !empty);
|
||||
}
|
||||
|
||||
private void generateNew() {
|
||||
@@ -362,7 +223,7 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
private void confirmBackup() {
|
||||
setDescription("Confirm backup by re-entering words");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size(), false));
|
||||
setContent(getMnemonicWordsEntry(wordEntriesProperty.get().size()));
|
||||
setExpanded(true);
|
||||
backButton.setVisible(true);
|
||||
generateButton.setVisible(false);
|
||||
@@ -415,145 +276,6 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
private static class WordEntry extends HBox {
|
||||
private static List<String> wordList;
|
||||
private final TextField wordField;
|
||||
private WordEntry nextEntry;
|
||||
private TextField nextField;
|
||||
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
setSpacing(10);
|
||||
Label label = new Label((wordNumber+1) + ".");
|
||||
label.setPrefWidth(22);
|
||||
label.setAlignment(Pos.CENTER_RIGHT);
|
||||
wordField = new TextField() {
|
||||
@Override
|
||||
public void paste() {
|
||||
Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||
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) {
|
||||
if(entry.nextField != null) {
|
||||
entry.nextField.requestFocus();
|
||||
}
|
||||
|
||||
entry.wordField.setText(word);
|
||||
entry = entry.nextEntry;
|
||||
if(entry == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.paste();
|
||||
}
|
||||
}
|
||||
};
|
||||
wordField.setMaxWidth(100);
|
||||
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
|
||||
String text = change.getText();
|
||||
// if text was added, fix the text to fit the requirements
|
||||
if(!text.isEmpty()) {
|
||||
String newText = text.replace(" ", "").toLowerCase();
|
||||
int carretPos = change.getCaretPosition() - text.length() + newText.length();
|
||||
change.setText(newText);
|
||||
// fix caret position based on difference in originally added text and fixed text
|
||||
change.selectRange(carretPos, carretPos);
|
||||
}
|
||||
return change;
|
||||
});
|
||||
wordField.setTextFormatter(formatter);
|
||||
|
||||
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
|
||||
autoCompletionBinding.setDelay(50);
|
||||
autoCompletionBinding.setOnAutoCompleted(event -> {
|
||||
if(nextField != null) {
|
||||
nextField.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
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))
|
||||
));
|
||||
|
||||
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
wordEntryList.set(wordNumber, newValue);
|
||||
});
|
||||
|
||||
this.getChildren().addAll(label, wordField);
|
||||
}
|
||||
|
||||
public TextField getEditor() {
|
||||
return wordField;
|
||||
}
|
||||
|
||||
public void setNextEntry(WordEntry nextEntry) {
|
||||
this.nextEntry = nextEntry;
|
||||
}
|
||||
|
||||
public void setNextField(TextField field) {
|
||||
this.nextField = field;
|
||||
}
|
||||
|
||||
public static boolean isValid(String word) {
|
||||
return wordList.contains(word);
|
||||
}
|
||||
}
|
||||
|
||||
private static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
|
||||
private final List<String> wordList;
|
||||
|
||||
public WordlistSuggestionProvider(List<String> wordList) {
|
||||
this.wordList = wordList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
for(String word : wordList) {
|
||||
if(word.startsWith(request.getUserText())) {
|
||||
suggestions.add(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
}
|
||||
|
||||
private class PassphraseEntry extends HBox {
|
||||
private final CustomTextField passphraseField;
|
||||
|
||||
public PassphraseEntry() {
|
||||
super();
|
||||
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setSpacing(10);
|
||||
Label passphraseLabel = new Label("Passphrase:");
|
||||
passphraseField = (CustomTextField) TextFields.createClearableTextField();
|
||||
passphraseProperty.bind(passphraseField.textProperty());
|
||||
passphraseField.setPromptText("Leave blank for none");
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
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!");
|
||||
|
||||
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
|
||||
}
|
||||
|
||||
public TextField getEditor() {
|
||||
return passphraseField;
|
||||
}
|
||||
}
|
||||
|
||||
private Node getDerivationEntry(List<ChildNumber> derivation) {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
@@ -591,30 +313,4 @@ public class MnemonicKeystoreImportPane extends TitledDescriptionPane {
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
private void showWordList(DeterministicSeed seed) {
|
||||
List<String> words = seed.getMnemonicCode();
|
||||
setContent(getMnemonicWordsEntry(words.size(), true));
|
||||
setExpanded(true);
|
||||
|
||||
for (int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
|
||||
wordEntry.getEditor().setText(words.get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
}
|
||||
}
|
||||
|
||||
public static Glyph getValidGlyph() {
|
||||
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||
validGlyph.getStyleClass().add("valid-checksum");
|
||||
validGlyph.setFontSize(12);
|
||||
return validGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getInvalidGlyph() {
|
||||
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
invalidGlyph.getStyleClass().add("invalid-checksum");
|
||||
invalidGlyph.setFontSize(12);
|
||||
return invalidGlyph;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Callback;
|
||||
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;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
|
||||
public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(MnemonicKeystorePane.class);
|
||||
|
||||
protected SplitMenuButton enterMnemonicButton;
|
||||
protected TilePane wordsPane;
|
||||
protected Label validLabel;
|
||||
protected Label invalidLabel;
|
||||
|
||||
protected SimpleListProperty<String> wordEntriesProperty;
|
||||
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty();
|
||||
protected IntegerProperty defaultWordSizeProperty;
|
||||
|
||||
public MnemonicKeystorePane(String title, String description, String content, String imageUrl) {
|
||||
super(title, description, content, imageUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
createEnterMnemonicButton();
|
||||
return enterMnemonicButton;
|
||||
}
|
||||
|
||||
private void createEnterMnemonicButton() {
|
||||
enterMnemonicButton = new SplitMenuButton();
|
||||
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
enterMnemonicButton.setText("Use 24 Words");
|
||||
defaultWordSizeProperty = new SimpleIntegerProperty(24);
|
||||
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
|
||||
enterMnemonicButton.setText("Use " + newValue + " Words");
|
||||
});
|
||||
enterMnemonicButton.setOnAction(event -> {
|
||||
enterMnemonic(defaultWordSizeProperty.get());
|
||||
});
|
||||
int[] numberWords = new int[] {24, 21, 18, 15, 12};
|
||||
for(int i = 0; i < numberWords.length; i++) {
|
||||
MenuItem item = new MenuItem("Use " + numberWords[i] + " Words");
|
||||
final int words = numberWords[i];
|
||||
item.setOnAction(event -> {
|
||||
defaultWordSizeProperty.set(words);
|
||||
enterMnemonic(words);
|
||||
});
|
||||
enterMnemonicButton.getItems().add(item);
|
||||
}
|
||||
enterMnemonicButton.getItems().add(new SeparatorMenuItem());
|
||||
MenuItem scanItem = new MenuItem("Scan QR...");
|
||||
scanItem.setOnAction(event -> {
|
||||
scanQR();
|
||||
});
|
||||
enterMnemonicButton.getItems().add(scanItem);
|
||||
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
||||
}
|
||||
|
||||
protected void scanQR() {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||
if(optionalResult.isPresent()) {
|
||||
QRScanDialog.Result result = optionalResult.get();
|
||||
if(result.seed != null) {
|
||||
showWordList(result.seed);
|
||||
Platform.runLater(() -> validLabel.requestFocus());
|
||||
} else if(result.exception != null) {
|
||||
log.error("Error scanning QR", result.exception);
|
||||
showErrorDialog("Error scanning QR", result.exception.getMessage());
|
||||
} else {
|
||||
AppServices.showErrorDialog("Invalid QR Code", "Cannot parse QR code into a seed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void showWordList(DeterministicSeed seed) {
|
||||
List<String> words = seed.getMnemonicCode();
|
||||
setContent(getMnemonicWordsEntry(words.size()));
|
||||
setExpanded(true);
|
||||
|
||||
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
|
||||
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
|
||||
wordEntry.getEditor().setText(words.get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
}
|
||||
}
|
||||
|
||||
protected void enterMnemonic(int numWords) {
|
||||
setDescription("Generate new or enter existing");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getMnemonicWordsEntry(numWords));
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
protected Node getMnemonicWordsEntry(int numWords) {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords/3);
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
|
||||
List<String> words = new ArrayList<>();
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
words.add("");
|
||||
}
|
||||
|
||||
ObservableList<String> wordEntryList = FXCollections.observableArrayList(words);
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
|
||||
}
|
||||
wordsPane.getChildren().addAll(wordEntries);
|
||||
|
||||
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);
|
||||
|
||||
AnchorPane buttonPane = new AnchorPane();
|
||||
buttonPane.setPadding(new Insets(0, 26, 0, 10));
|
||||
|
||||
HBox leftBox = new HBox(10);
|
||||
leftBox.getChildren().addAll(createLeftButtons());
|
||||
|
||||
buttonPane.getChildren().add(leftBox);
|
||||
AnchorPane.setLeftAnchor(leftBox, 0.0);
|
||||
|
||||
validLabel = new Label("Valid checksum", getValidGlyph());
|
||||
validLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
validLabel.setGraphicTextGap(5.0);
|
||||
validLabel.managedProperty().bind(validLabel.visibleProperty());
|
||||
validLabel.setVisible(false);
|
||||
buttonPane.getChildren().add(validLabel);
|
||||
AnchorPane.setTopAnchor(validLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(validLabel, 0.0);
|
||||
|
||||
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
|
||||
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
invalidLabel.setGraphicTextGap(5.0);
|
||||
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
|
||||
invalidLabel.setVisible(false);
|
||||
buttonPane.getChildren().add(invalidLabel);
|
||||
AnchorPane.setTopAnchor(invalidLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(invalidLabel, 0.0);
|
||||
|
||||
wordEntriesProperty.addListener((ListChangeListener<String>) c -> {
|
||||
boolean empty = true;
|
||||
boolean validWords = true;
|
||||
boolean validChecksum = false;
|
||||
for(String word : wordEntryList) {
|
||||
if(!word.isEmpty()) {
|
||||
empty = false;
|
||||
}
|
||||
|
||||
if(!WordEntry.isValid(word)) {
|
||||
validWords = false;
|
||||
}
|
||||
}
|
||||
|
||||
onWordChange(empty, validWords, validChecksum);
|
||||
});
|
||||
|
||||
HBox rightBox = new HBox();
|
||||
rightBox.setSpacing(10);
|
||||
rightBox.getChildren().addAll(createRightButtons());
|
||||
|
||||
buttonPane.getChildren().add(rightBox);
|
||||
AnchorPane.setRightAnchor(rightBox, 0.0);
|
||||
|
||||
vBox.getChildren().add(buttonPane);
|
||||
|
||||
Platform.runLater(() -> wordEntries.get(0).getEditor().requestFocus());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(vBox);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
protected List<Node> createLeftButtons() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
protected List<Node> createRightButtons() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
//nothing by default
|
||||
}
|
||||
|
||||
protected static class WordEntry extends HBox {
|
||||
private static List<String> wordList;
|
||||
private final TextField wordField;
|
||||
private WordEntry nextEntry;
|
||||
private TextField nextField;
|
||||
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
setSpacing(10);
|
||||
Label label = new Label((wordNumber+1) + ".");
|
||||
label.setPrefWidth(22);
|
||||
label.setAlignment(Pos.CENTER_RIGHT);
|
||||
wordField = new TextField() {
|
||||
@Override
|
||||
public void paste() {
|
||||
Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||
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) {
|
||||
if(entry.nextField != null) {
|
||||
entry.nextField.requestFocus();
|
||||
}
|
||||
|
||||
entry.wordField.setText(word);
|
||||
entry = entry.nextEntry;
|
||||
if(entry == null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.paste();
|
||||
}
|
||||
}
|
||||
};
|
||||
wordField.setMaxWidth(100);
|
||||
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
|
||||
String text = change.getText();
|
||||
// if text was added, fix the text to fit the requirements
|
||||
if(!text.isEmpty()) {
|
||||
String newText = text.replace(" ", "").toLowerCase(Locale.ROOT);
|
||||
int carretPos = change.getCaretPosition() - text.length() + newText.length();
|
||||
change.setText(newText);
|
||||
// fix caret position based on difference in originally added text and fixed text
|
||||
change.selectRange(carretPos, carretPos);
|
||||
}
|
||||
return change;
|
||||
});
|
||||
wordField.setTextFormatter(formatter);
|
||||
|
||||
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList));
|
||||
autoCompletionBinding.setDelay(50);
|
||||
autoCompletionBinding.setOnAutoCompleted(event -> {
|
||||
if(nextField != null) {
|
||||
nextField.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
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))
|
||||
));
|
||||
|
||||
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
wordEntryList.set(wordNumber, newValue);
|
||||
});
|
||||
|
||||
this.getChildren().addAll(label, wordField);
|
||||
}
|
||||
|
||||
public TextField getEditor() {
|
||||
return wordField;
|
||||
}
|
||||
|
||||
public void setNextEntry(WordEntry nextEntry) {
|
||||
this.nextEntry = nextEntry;
|
||||
}
|
||||
|
||||
public void setNextField(TextField field) {
|
||||
this.nextField = field;
|
||||
}
|
||||
|
||||
public static boolean isValid(String word) {
|
||||
return wordList.contains(word);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
|
||||
private final List<String> wordList;
|
||||
|
||||
public WordlistSuggestionProvider(List<String> wordList) {
|
||||
this.wordList = wordList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
for(String word : wordList) {
|
||||
if(word.startsWith(request.getUserText())) {
|
||||
suggestions.add(word);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
}
|
||||
|
||||
protected class PassphraseEntry extends HBox {
|
||||
private final CustomTextField passphraseField;
|
||||
|
||||
public PassphraseEntry() {
|
||||
super();
|
||||
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setSpacing(10);
|
||||
Label passphraseLabel = new Label("Passphrase:");
|
||||
passphraseField = (CustomTextField) TextFields.createClearableTextField();
|
||||
passphraseProperty.bind(passphraseField.textProperty());
|
||||
passphraseField.setPromptText("Leave blank for none");
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
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!");
|
||||
|
||||
getChildren().addAll(passphraseLabel, passphraseField, helpLabel);
|
||||
}
|
||||
|
||||
public TextField getEditor() {
|
||||
return passphraseField;
|
||||
}
|
||||
}
|
||||
|
||||
public static Glyph getValidGlyph() {
|
||||
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||
validGlyph.getStyleClass().add("success");
|
||||
validGlyph.setFontSize(12);
|
||||
return validGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getInvalidGlyph() {
|
||||
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
invalidGlyph.getStyleClass().add("failure");
|
||||
invalidGlyph.setFontSize(12);
|
||||
return invalidGlyph;
|
||||
}
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
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.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreMnemonicImport;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import javafx.collections.FXCollections;
|
||||
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.Region;
|
||||
import javafx.util.StringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
private static final Logger log = LoggerFactory.getLogger(MnemonicWalletKeystoreImportPane.class);
|
||||
|
||||
private final KeystoreMnemonicImport importer;
|
||||
|
||||
private Button discoverButton;
|
||||
private Button importButton;
|
||||
|
||||
public MnemonicWalletKeystoreImportPane(KeystoreMnemonicImport importer) {
|
||||
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enterMnemonic(int numWords) {
|
||||
super.enterMnemonic(numWords);
|
||||
setDescription("Enter word list");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Node> createRightButtons() {
|
||||
discoverButton = new Button("Discover Wallet");
|
||||
discoverButton.setDisable(true);
|
||||
discoverButton.setDefaultButton(true);
|
||||
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
|
||||
discoverButton.setOnAction(event -> {
|
||||
discoverWallet();
|
||||
});
|
||||
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
|
||||
discoverButton.setTooltip(new Tooltip("Look for existing transactions from the provided word list"));
|
||||
discoverButton.visibleProperty().bind(AppServices.onlineProperty());
|
||||
|
||||
importButton = new Button("Import Wallet");
|
||||
importButton.setDisable(true);
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
importButton.visibleProperty().bind(discoverButton.visibleProperty().not());
|
||||
importButton.setOnAction(event -> {
|
||||
setContent(getScriptTypeEntry());
|
||||
setExpanded(true);
|
||||
});
|
||||
|
||||
return List.of(discoverButton, importButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
importer.getKeystore(ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
|
||||
invalidLabel.setText("Unsupported Electrum seed");
|
||||
invalidLabel.setTooltip(new Tooltip("Seeds created in Electrum do not follow the BIP39 standard. Import the Electrum wallet file directly."));
|
||||
} else {
|
||||
invalidLabel.setText("Invalid checksum");
|
||||
invalidLabel.setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
discoverButton.setDisable(!validChecksum || !AppServices.isConnected());
|
||||
importButton.setDisable(!validChecksum);
|
||||
validLabel.setVisible(validChecksum);
|
||||
invalidLabel.setVisible(!validChecksum && !empty);
|
||||
}
|
||||
|
||||
private void discoverWallet() {
|
||||
discoverButton.setDisable(true);
|
||||
discoverButton.setMaxHeight(discoverButton.getHeight());
|
||||
ProgressIndicator progressIndicator = new ProgressIndicator(0);
|
||||
progressIndicator.getStyleClass().add("button-progress");
|
||||
discoverButton.setGraphic(progressIndicator);
|
||||
List<Wallet> wallets = new ArrayList<>();
|
||||
|
||||
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
|
||||
derivations.add(List.of(new ChildNumber(0, true)));
|
||||
|
||||
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE)) {
|
||||
for(List<ChildNumber> derivation : derivations) {
|
||||
try {
|
||||
Wallet wallet = getWallet(scriptType, derivation);
|
||||
wallets.add(wallet);
|
||||
} catch(ImportException e) {
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) {
|
||||
errorMessage = "Invalid word list - checksum incorrect";
|
||||
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Import Error", errorMessage + ".");
|
||||
discoverButton.setDisable(!AppServices.isConnected());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallets);
|
||||
progressIndicator.progressProperty().bind(walletDiscoveryService.progressProperty());
|
||||
walletDiscoveryService.setOnSucceeded(successEvent -> {
|
||||
discoverButton.setGraphic(null);
|
||||
Optional<Wallet> optWallet = walletDiscoveryService.getValue();
|
||||
if(optWallet.isPresent()) {
|
||||
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);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
setContent(getScriptTypeEntry());
|
||||
setExpanded(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
walletDiscoveryService.setOnFailed(failedEvent -> {
|
||||
discoverButton.setGraphic(null);
|
||||
log.error("Failed to discover wallets", failedEvent.getSource().getException());
|
||||
setError("Failed to discover wallets", failedEvent.getSource().getException().getMessage());
|
||||
});
|
||||
walletDiscoveryService.start();
|
||||
}
|
||||
|
||||
private Wallet getWallet(ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
|
||||
Wallet wallet = new Wallet("");
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setScriptType(scriptType);
|
||||
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
|
||||
return wallet;
|
||||
}
|
||||
|
||||
private Node getScriptTypeEntry() {
|
||||
Label label = new Label("Script Type:");
|
||||
|
||||
HBox fieldBox = new HBox(5);
|
||||
fieldBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
|
||||
if(scriptTypeComboBox.getItems().contains(ScriptType.P2WPKH)) {
|
||||
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
|
||||
}
|
||||
scriptTypeComboBox.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(ScriptType scriptType) {
|
||||
return scriptType == null ? "" : scriptType.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
scriptTypeComboBox.setMaxWidth(170);
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setHelpText("P2WPKH is a Native Segwit type and is usually the best choice for new wallets.\nP2SH-P2WPKH is a Wrapped Segwit type and is a reasonable choice for the widest compatibility.\nP2PKH is a Legacy type and should be avoided for new wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
|
||||
|
||||
Region region = new Region();
|
||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||
|
||||
Button importMnemonicButton = new Button("Import");
|
||||
importMnemonicButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
try {
|
||||
ScriptType scriptType = scriptTypeComboBox.getValue();
|
||||
Wallet wallet = getWallet(scriptType, scriptType.getDefaultDerivation());
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing mnemonic", e);
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Import Error", errorMessage);
|
||||
importButton.setDisable(false);
|
||||
}
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().addAll(label, fieldBox, region, importMnemonicButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
public class NumberCell extends TreeTableCell<Entry, Number> {
|
||||
public NumberCell() {
|
||||
super();
|
||||
getStyleClass().add("number-cell");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
getStyleClass().add("number-field");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Number amount, boolean empty) {
|
||||
super.updateItem(amount, empty);
|
||||
|
||||
if(empty || amount == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(amount.toString());
|
||||
setGraphic(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymService;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.concurrent.Service;
|
||||
@@ -39,7 +40,7 @@ public class PayNymAvatar extends StackPane {
|
||||
String cacheId = getCacheId(paymentCode, getPrefWidth());
|
||||
if(paymentCodeCache.containsKey(cacheId)) {
|
||||
setImage(paymentCodeCache.get(cacheId));
|
||||
} else {
|
||||
} else if(AppServices.isConnected()) {
|
||||
PayNymAvatarService payNymAvatarService = new PayNymAvatarService(paymentCode, getPrefWidth());
|
||||
payNymAvatarService.setOnRunning(runningEvent -> {
|
||||
getChildren().clear();
|
||||
@@ -48,7 +49,7 @@ public class PayNymAvatar extends StackPane {
|
||||
setImage(payNymAvatarService.getValue());
|
||||
});
|
||||
payNymAvatarService.setOnFailed(failedEvent -> {
|
||||
log.error("Error", failedEvent.getSource().getException());
|
||||
log.debug("Error loading PayNym avatar", failedEvent.getSource().getException());
|
||||
});
|
||||
payNymAvatarService.start();
|
||||
}
|
||||
@@ -75,6 +76,14 @@ public class PayNymAvatar extends StackPane {
|
||||
this.paymentCodeProperty.set(paymentCode);
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
setPaymentCode(PaymentCode.fromString(paymentCode.toString()));
|
||||
}
|
||||
|
||||
public void clearPaymentCode() {
|
||||
this.paymentCodeProperty.set(null);
|
||||
}
|
||||
|
||||
private static String getCacheId(PaymentCode paymentCode, double width) {
|
||||
return paymentCode.toString();
|
||||
}
|
||||
@@ -109,8 +118,12 @@ public class PayNymAvatar extends StackPane {
|
||||
}
|
||||
|
||||
synchronized(lock) {
|
||||
String url = "https://paynym.is/" + paymentCodeStr + "/avatar";
|
||||
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);
|
||||
|
||||
@@ -1,29 +1,40 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.soroban.PayNym;
|
||||
import com.sparrowwallet.sparrow.soroban.PayNymController;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletLabelChangedEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNym;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymController;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class PayNymCell extends ListCell<PayNym> {
|
||||
private final PayNymController payNymController;
|
||||
private final boolean contact;
|
||||
|
||||
public PayNymCell(PayNymController payNymController) {
|
||||
public PayNymCell(PayNymController payNymController, boolean contact) {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setContentDisplay(ContentDisplay.LEFT);
|
||||
getStyleClass().add("paynym-cell");
|
||||
setPrefHeight(50);
|
||||
this.payNymController = payNymController;
|
||||
this.contact = contact;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(PayNym payNym, boolean empty) {
|
||||
super.updateItem(payNym, empty);
|
||||
|
||||
getStyleClass().remove("unlinked");
|
||||
|
||||
if(empty || payNym == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
@@ -43,7 +54,7 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||
labelBox.getChildren().add(label);
|
||||
pane.setLeft(labelBox);
|
||||
|
||||
if(getListView().getUserData() == Boolean.TRUE) {
|
||||
if(getListView().getUserData() == Boolean.TRUE || (!contact && payNymController != null && payNymController.isFollowing(payNym) == Boolean.FALSE)) {
|
||||
HBox hBox = new HBox();
|
||||
hBox.setAlignment(Pos.CENTER);
|
||||
Button button = new Button("Add Contact");
|
||||
@@ -53,10 +64,70 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||
button.setDisable(true);
|
||||
payNymController.followPayNym(payNym.paymentCode());
|
||||
});
|
||||
} else if(contact && payNymController != null) {
|
||||
HBox hBox = new HBox();
|
||||
hBox.setAlignment(Pos.CENTER);
|
||||
pane.setRight(hBox);
|
||||
|
||||
if(payNymController.isLinked(payNym)) {
|
||||
Label linkedLabel = new Label("Linked", getLinkGlyph());
|
||||
linkedLabel.setTooltip(new Tooltip("You can send non-collaboratively to this contact."));
|
||||
hBox.getChildren().add(linkedLabel);
|
||||
} else {
|
||||
Button linkButton = new Button("Link Contact", getLinkGlyph());
|
||||
linkButton.setTooltip(new Tooltip("Create a transaction that will enable you to send non-collaboratively to this contact."));
|
||||
hBox.getChildren().add(linkButton);
|
||||
linkButton.setOnAction(event -> {
|
||||
linkButton.setDisable(true);
|
||||
payNymController.linkPayNym(payNym);
|
||||
});
|
||||
|
||||
if(payNymController.isSelectLinkedOnly()) {
|
||||
getStyleClass().add("unlinked");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setText(null);
|
||||
setGraphic(pane);
|
||||
|
||||
if(contact && payNymController != null && payNymController.isLinked(payNym)) {
|
||||
setContextMenu(new PayNymCellContextMenu(payNym));
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Glyph getLinkGlyph() {
|
||||
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.LINK);
|
||||
failGlyph.setFontSize(12);
|
||||
return failGlyph;
|
||||
}
|
||||
|
||||
private class PayNymCellContextMenu extends ContextMenu {
|
||||
public PayNymCellContextMenu(PayNym payNym) {
|
||||
MenuItem rename = new MenuItem("Rename Contact...");
|
||||
rename.setOnAction(event -> {
|
||||
WalletLabelDialog walletLabelDialog = new WalletLabelDialog(payNym.nymName(), "Contact");
|
||||
Optional<String> optLabel = walletLabelDialog.showAndWait();
|
||||
if(optLabel.isPresent()) {
|
||||
int index = getListView().getItems().indexOf(payNym);
|
||||
for(Wallet childWallet : payNymController.getMasterWallet().getChildWallets()) {
|
||||
if(childWallet.isBip47()
|
||||
&& childWallet.getKeystores().get(0).getExternalPaymentCode().equals(payNym.paymentCode())
|
||||
&& (childWallet.getLabel() == null || childWallet.getLabel().startsWith(payNym.nymName()))) {
|
||||
childWallet.setLabel(optLabel.get() + " " + childWallet.getScriptType().getName());
|
||||
EventManager.get().post(new WalletLabelChangedEvent(childWallet));
|
||||
}
|
||||
}
|
||||
|
||||
payNymController.updateFollowing();
|
||||
getListView().getSelectionModel().select(index);
|
||||
}
|
||||
});
|
||||
|
||||
getItems().add(rename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
|
||||
public class PaymentCodeTextField extends CopyableTextField {
|
||||
private String paymentCodeStr;
|
||||
|
||||
public void setPaymentCode(PaymentCode paymentCode) {
|
||||
this.paymentCodeStr = paymentCode.toString();
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
this.paymentCodeStr = paymentCode.toString();
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
private void setPaymentCodeString() {
|
||||
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
|
||||
setText(abbrevPcode);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.io.Files;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.crypto.BIP38;
|
||||
import com.sparrowwallet.drongo.crypto.DumpedPrivateKey;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
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.net.ElectrumServer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.event.ActionEvent;
|
||||
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 javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR;
|
||||
|
||||
public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
private static final Logger log = LoggerFactory.getLogger(PrivateKeySweepDialog.class);
|
||||
|
||||
private final TextArea key;
|
||||
private final ComboBox<ScriptType> keyScriptType;
|
||||
private final CopyableLabel keyAddress;
|
||||
private final ComboBoxTextField toAddress;
|
||||
private final ComboBox<Wallet> toWallet;
|
||||
|
||||
public PrivateKeySweepDialog(Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText("Sweep Private Key");
|
||||
|
||||
Image image = new Image("image/seed.png", 50, 50, false, false);
|
||||
if(!image.isError()) {
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setSmooth(false);
|
||||
imageView.setImage(image);
|
||||
dialogPane.setGraphic(imageView);
|
||||
}
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
fieldset.setSpacing(10);
|
||||
|
||||
Field keyField = new Field();
|
||||
keyField.setText("Private Key:");
|
||||
key = new TextArea();
|
||||
key.setWrapText(true);
|
||||
key.setPromptText("Wallet Import Format (WIF) or BIP38 encrypted key");
|
||||
key.setPrefRowCount(2);
|
||||
key.getStyleClass().add("fixed-width");
|
||||
HBox keyBox = new HBox(5);
|
||||
VBox keyButtonBox = new VBox(5);
|
||||
Button scanKey = new Button("", getGlyph(FontAwesome5.Glyph.CAMERA));
|
||||
scanKey.setOnAction(event -> scanPrivateKey());
|
||||
Button readKey = new Button("", getGlyph(FontAwesome5.Glyph.FILE_IMPORT));
|
||||
readKey.setOnAction(event -> readPrivateKey());
|
||||
keyButtonBox.getChildren().addAll(scanKey, readKey);
|
||||
keyBox.getChildren().addAll(key, keyButtonBox);
|
||||
HBox.setHgrow(key, Priority.ALWAYS);
|
||||
keyField.getInputs().add(keyBox);
|
||||
|
||||
Field keyScriptTypeField = new Field();
|
||||
keyScriptTypeField.setText("Script Type:");
|
||||
keyScriptType = new ComboBox<>();
|
||||
keyScriptType.setItems(FXCollections.observableList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
|
||||
keyScriptTypeField.getInputs().add(keyScriptType);
|
||||
|
||||
keyScriptType.setConverter(new StringConverter<ScriptType>() {
|
||||
@Override
|
||||
public String toString(ScriptType scriptType) {
|
||||
return scriptType == null ? "" : scriptType.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
Field addressField = new Field();
|
||||
addressField.setText("Address:");
|
||||
keyAddress = new CopyableLabel();
|
||||
keyAddress.getStyleClass().add("fixed-width");
|
||||
addressField.getInputs().add(keyAddress);
|
||||
|
||||
Field toAddressField = new Field();
|
||||
toAddressField.setText("Sweep to:");
|
||||
toAddress = new ComboBoxTextField();
|
||||
toAddress.getStyleClass().add("fixed-width");
|
||||
toWallet = new ComboBox<>();
|
||||
toWallet.setItems(FXCollections.observableList(AppServices.get().getOpenWallets().keySet().stream()
|
||||
.filter(w -> !w.isWhirlpoolChildWallet() && !w.isBip47()).collect(Collectors.toList())));
|
||||
toAddress.setComboProperty(toWallet);
|
||||
toWallet.prefWidthProperty().bind(toAddress.widthProperty());
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().addAll(toWallet, toAddress);
|
||||
toAddressField.getInputs().add(stackPane);
|
||||
|
||||
fieldset.getChildren().addAll(keyField, keyScriptTypeField, addressField, toAddressField);
|
||||
form.getChildren().add(fieldset);
|
||||
dialogPane.setContent(form);
|
||||
|
||||
ButtonType createButtonType = new javafx.scene.control.ButtonType("Create Transaction", ButtonBar.ButtonData.APPLY);
|
||||
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType, createButtonType);
|
||||
|
||||
Button createButton = (Button) dialogPane.lookupButton(createButtonType);
|
||||
createButton.setDefaultButton(true);
|
||||
createButton.setDisable(true);
|
||||
createButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
createTransaction();
|
||||
event.consume();
|
||||
});
|
||||
|
||||
key.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(isEncryptedKey()) {
|
||||
decryptKey();
|
||||
}
|
||||
|
||||
boolean isValidKey = isValidKey();
|
||||
createButton.setDisable(!isValidKey || !isValidToAddress());
|
||||
if(isValidKey) {
|
||||
setFromAddress();
|
||||
}
|
||||
});
|
||||
|
||||
keyScriptType.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(isValidKey()) {
|
||||
setFromAddress();
|
||||
}
|
||||
});
|
||||
|
||||
toAddress.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
createButton.setDisable(!isValidKey() || !isValidToAddress());
|
||||
});
|
||||
|
||||
toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> {
|
||||
if(selectedWallet != null) {
|
||||
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
});
|
||||
|
||||
keyScriptType.setValue(ScriptType.P2PKH);
|
||||
if(wallet != null) {
|
||||
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
setResultConverter(dialogButton -> null);
|
||||
dialogPane.setPrefWidth(680);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(key, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid private Key", !key.getText().isEmpty() && !isValidKey()));
|
||||
validationSupport.registerValidator(toAddress, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid address", !toAddress.getText().isEmpty() && !isValidToAddress()));
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isValidKey() {
|
||||
try {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isEncryptedKey() {
|
||||
return key.getText().length() == 58 && key.getText().startsWith("6P");
|
||||
}
|
||||
|
||||
private void decryptKey() {
|
||||
PassphraseDialog passphraseDialog = new PassphraseDialog();
|
||||
Optional<String> optPassphrase = passphraseDialog.showAndWait();
|
||||
if(optPassphrase.isPresent()) {
|
||||
try {
|
||||
DumpedPrivateKey decryptedKey = BIP38.decrypt(optPassphrase.get(), key.getText());
|
||||
Platform.runLater(() -> key.setText(decryptedKey.toString()));
|
||||
} catch(Exception e) {
|
||||
log.error("Failed to decrypt BIP38 key", e);
|
||||
AppServices.showErrorDialog("Failed to decrypt BIP38 key", e.getMessage());
|
||||
Platform.runLater(() -> key.setText(""));
|
||||
}
|
||||
} else {
|
||||
Platform.runLater(() -> key.setText(""));
|
||||
}
|
||||
}
|
||||
|
||||
private DumpedPrivateKey getPrivateKey() {
|
||||
return DumpedPrivateKey.fromBase58(key.getText());
|
||||
}
|
||||
|
||||
private boolean isValidToAddress() {
|
||||
try {
|
||||
Address address = getToAddress();
|
||||
return true;
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Address getToAddress() throws InvalidAddressException {
|
||||
return Address.fromString(toAddress.getText());
|
||||
}
|
||||
|
||||
private void setFromAddress() {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
ScriptType scriptType = keyScriptType.getValue();
|
||||
Address address = scriptType.getAddress(privateKey.getKey());
|
||||
keyAddress.setText(address.toString());
|
||||
}
|
||||
|
||||
private void scanPrivateKey() {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
Optional<QRScanDialog.Result> result = qrScanDialog.showAndWait();
|
||||
if(result.isPresent() && result.get().payload != null) {
|
||||
key.setText(result.get().payload);
|
||||
}
|
||||
}
|
||||
|
||||
private void readPrivateKey() {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Private Key File");
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
if(file != null) {
|
||||
if(file.length() > 1024) {
|
||||
AppServices.showErrorDialog("Invalid private key file", "This file does not contain a valid private key.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
key.setText(Files.asCharSource(file, StandardCharsets.UTF_8).read().trim());
|
||||
} catch(IOException e) {
|
||||
AppServices.showErrorDialog("Error reading private key file", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createTransaction() {
|
||||
try {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
ScriptType scriptType = keyScriptType.getValue();
|
||||
Address fromAddress = scriptType.getAddress(privateKey.getKey());
|
||||
Address destAddress = getToAddress();
|
||||
|
||||
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress);
|
||||
addressUtxosService.setOnSucceeded(successEvent -> {
|
||||
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
|
||||
});
|
||||
addressUtxosService.setOnFailed(failedEvent -> {
|
||||
log.error("Error retrieving outputs for address " + fromAddress, failedEvent.getSource().getException());
|
||||
AppServices.showErrorDialog("Error retrieving outputs for address", failedEvent.getSource().getException().getMessage());
|
||||
});
|
||||
addressUtxosService.start();
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating sweep transaction", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Address destAddress) {
|
||||
ECKey pubKey = ECKey.fromPublicOnly(privKey);
|
||||
|
||||
Transaction noFeeTransaction = new Transaction();
|
||||
long total = 0;
|
||||
for(TransactionOutput txOutput : txOutputs) {
|
||||
scriptType.addSpendingInput(noFeeTransaction, txOutput, pubKey, TransactionSignature.dummy(scriptType == P2TR ? TransactionSignature.Type.SCHNORR : TransactionSignature.Type.ECDSA));
|
||||
total += txOutput.getValue();
|
||||
}
|
||||
|
||||
TransactionOutput sweepOutput = new TransactionOutput(noFeeTransaction, total, destAddress.getOutputScript());
|
||||
noFeeTransaction.addOutput(sweepOutput);
|
||||
|
||||
Double feeRate = AppServices.getDefaultFeeRate();
|
||||
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
||||
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
fee++;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Transaction transaction = new Transaction();
|
||||
transaction.setVersion(2);
|
||||
transaction.setLocktime(AppServices.getCurrentBlockHeight() == null ? 0 : AppServices.getCurrentBlockHeight());
|
||||
for(TransactionInput txInput : noFeeTransaction.getInputs()) {
|
||||
transaction.addInput(txInput);
|
||||
}
|
||||
transaction.addOutput(new TransactionOutput(transaction, total - fee, destAddress.getOutputScript()));
|
||||
|
||||
PSBT psbt = new PSBT(transaction);
|
||||
for(int i = 0; i < txOutputs.size(); i++) {
|
||||
TransactionOutput utxoOutput = txOutputs.get(i);
|
||||
TransactionInput txInput = transaction.getInputs().get(i);
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
|
||||
psbtInput.setWitnessUtxo(utxoOutput);
|
||||
|
||||
if(ScriptType.P2SH.isScriptType(utxoOutput.getScript())) {
|
||||
psbtInput.setRedeemScript(txInput.getScriptSig().getFirstNestedScript());
|
||||
}
|
||||
|
||||
if(txInput.getWitness() != null) {
|
||||
psbtInput.setWitnessScript(txInput.getWitness().getWitnessScript());
|
||||
}
|
||||
|
||||
if(!psbtInput.sign(scriptType.getOutputKey(privKey))) {
|
||||
AppServices.showErrorDialog("Failed to sign", "Failed to sign for transaction output " + utxoOutput.getHash() + ":" + utxoOutput.getIndex());
|
||||
return;
|
||||
}
|
||||
|
||||
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
|
||||
|
||||
Transaction finalizeTransaction = new Transaction();
|
||||
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
|
||||
psbtInput.setFinalScriptSig(finalizedTxInput.getScriptSig());
|
||||
psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness());
|
||||
}
|
||||
|
||||
setResult(psbt.extractTransaction());
|
||||
}
|
||||
|
||||
public Glyph getGlyph(FontAwesome5.Glyph glyphEnum) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphEnum);
|
||||
glyph.setFontSize(12);
|
||||
return glyph;
|
||||
}
|
||||
|
||||
public class PassphraseDialog extends Dialog<String> {
|
||||
private final CustomPasswordField passphrase;
|
||||
|
||||
public PassphraseDialog() {
|
||||
this.passphrase = new ViewPasswordField();
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
setTitle("BIP38 Passphrase");
|
||||
dialogPane.setHeaderText("Enter the BIP38 passphrase for this key:");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
|
||||
dialogPane.setPrefWidth(380);
|
||||
dialogPane.setPrefHeight(200);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.KEY);
|
||||
key.setFontSize(50);
|
||||
dialogPane.setGraphic(key);
|
||||
|
||||
final VBox content = new VBox(10);
|
||||
content.setPrefHeight(50);
|
||||
content.getChildren().add(passphrase);
|
||||
|
||||
dialogPane.setContent(content);
|
||||
Platform.runLater(passphrase::requestFocus);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? passphrase.getText() : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public class ProgressTimer extends ProgressIndicator {
|
||||
}
|
||||
|
||||
public void start(EventHandler<ActionEvent> onFinished) {
|
||||
getStyleClass().remove("warn");
|
||||
timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(progressProperty(), 0)),
|
||||
new KeyFrame(Duration.seconds(getSeconds() * 0.8), e -> getStyleClass().add("warn")),
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class QRDisplayDialog extends Dialog<UR> {
|
||||
@@ -78,12 +79,7 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
if(encoder.isSinglePart()) {
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else {
|
||||
animateQRService = new AnimateQRService();
|
||||
animateQRService.setPeriod(Duration.millis(ANIMATION_PERIOD_MILLIS));
|
||||
animateQRService.start();
|
||||
setOnCloseRequest(event -> {
|
||||
animateQRService.cancel();
|
||||
});
|
||||
createAnimateQRService();
|
||||
}
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
@@ -124,10 +120,19 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? ur : null);
|
||||
}
|
||||
|
||||
private void createAnimateQRService() {
|
||||
animateQRService = new AnimateQRService();
|
||||
animateQRService.setPeriod(Duration.millis(ANIMATION_PERIOD_MILLIS));
|
||||
animateQRService.start();
|
||||
setOnCloseRequest(event -> {
|
||||
animateQRService.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
private void nextPart() {
|
||||
if(!useLegacyEncoding) {
|
||||
String fragment = encoder.nextPart();
|
||||
currentPart = fragment.toUpperCase();
|
||||
currentPart = fragment.toUpperCase(Locale.ROOT);
|
||||
} else {
|
||||
currentPart = legacyParts[legacyPartIndex];
|
||||
legacyPartIndex++;
|
||||
@@ -137,7 +142,7 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
}
|
||||
}
|
||||
|
||||
private Image getQrCode(String fragment) {
|
||||
protected Image getQrCode(String fragment) {
|
||||
try {
|
||||
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
||||
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);
|
||||
@@ -169,6 +174,8 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
|
||||
nextPart();
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else if(animateQRService == null) {
|
||||
createAnimateQRService();
|
||||
} else if(!animateQRService.isRunning()) {
|
||||
animateQRService.reset();
|
||||
animateQRService.start();
|
||||
@@ -185,6 +192,8 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
}
|
||||
|
||||
qrImageView.setImage(getQrCode(currentPart));
|
||||
} else if(animateQRService == null) {
|
||||
createAnimateQRService();
|
||||
} else if(!animateQRService.isRunning()) {
|
||||
animateQRService.reset();
|
||||
animateQRService.start();
|
||||
@@ -235,11 +244,11 @@ public class QRDisplayDialog extends Dialog<UR> {
|
||||
legacy.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
protected static Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.SeedQR;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.hummingbird.LegacyURDecoder;
|
||||
import com.sparrowwallet.hummingbird.registry.*;
|
||||
@@ -184,7 +185,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
String qrtext = qrResult.getText();
|
||||
Matcher partMatcher = PART_PATTERN.matcher(qrtext);
|
||||
|
||||
if(qrtext.toLowerCase().startsWith(UR.UR_PREFIX)) {
|
||||
if(qrtext.toLowerCase(Locale.ROOT).startsWith(UR.UR_PREFIX)) {
|
||||
if(LegacyURDecoder.isLegacyURFragment(qrtext)) {
|
||||
legacyDecoder.receivePart(qrtext);
|
||||
Platform.runLater(() -> percentComplete.setValue(legacyDecoder.getPercentComplete()));
|
||||
@@ -255,6 +256,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
BitcoinURI bitcoinURI;
|
||||
Address address;
|
||||
ExtendedKey extendedKey;
|
||||
DeterministicSeed seed;
|
||||
try {
|
||||
extendedKey = ExtendedKey.fromDescriptor(qrtext);
|
||||
result = new Result(extendedKey);
|
||||
@@ -332,6 +334,22 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
//Ignore, not parseable as base43 decoded bytes
|
||||
}
|
||||
|
||||
try {
|
||||
seed = SeedQR.getSeed(qrtext);
|
||||
result = new Result(seed);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//Ignore, not parseable as a SeedQR
|
||||
}
|
||||
|
||||
try {
|
||||
seed = SeedQR.getSeed(qrResult.getRawBytes());
|
||||
result = new Result(seed);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
//Ignore, not parseable as a CompactSeedQR
|
||||
}
|
||||
|
||||
result = new Result(qrtext);
|
||||
}
|
||||
}
|
||||
@@ -399,6 +417,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
CryptoAccount cryptoAccount = (CryptoAccount)ur.decodeFromRegistry();
|
||||
List<Wallet> wallets = getWallets(cryptoAccount);
|
||||
return new Result(wallets);
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_SEED)) {
|
||||
CryptoSeed cryptoSeed = (CryptoSeed)ur.decodeFromRegistry();
|
||||
DeterministicSeed seed = getSeed(cryptoSeed);
|
||||
return new Result(seed);
|
||||
} else if(urRegistryType.equals(RegistryType.CRYPTO_BIP39)) {
|
||||
CryptoBip39 cryptoBip39 = (CryptoBip39)ur.decodeFromRegistry();
|
||||
DeterministicSeed seed = getSeed(cryptoBip39);
|
||||
return new Result(seed);
|
||||
} else {
|
||||
log.error("Unsupported UR type " + urRegistryType);
|
||||
return new Result(new URException("UR type " + urRegistryType + " is not supported"));
|
||||
@@ -479,18 +505,8 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
List<Wallet> wallets = new ArrayList<>();
|
||||
String masterFingerprint = Utils.bytesToHex(cryptoAccount.getMasterFingerprint());
|
||||
for(CryptoOutput cryptoOutput : cryptoAccount.getOutputDescriptors()) {
|
||||
Wallet wallet = new Wallet();
|
||||
OutputDescriptor outputDescriptor = getOutputDescriptor(cryptoOutput);
|
||||
if(outputDescriptor.isMultisig()) {
|
||||
throw new IllegalStateException("Multisig output descriptors are unsupported in CryptoAccount");
|
||||
}
|
||||
|
||||
ExtendedKey extendedKey = outputDescriptor.getSingletonExtendedPublicKey();
|
||||
wallet.setScriptType(outputDescriptor.getScriptType());
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(outputDescriptor.getKeyDerivation(extendedKey).getDerivation())));
|
||||
keystore.setExtendedPublicKey(extendedKey);
|
||||
wallet.getKeystores().add(keystore);
|
||||
Wallet wallet = outputDescriptor.toKeystoreWallet(masterFingerprint);
|
||||
wallets.add(wallet);
|
||||
}
|
||||
|
||||
@@ -531,6 +547,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private DeterministicSeed getSeed(CryptoSeed cryptoSeed) {
|
||||
return new DeterministicSeed(cryptoSeed.getSeed(), null, cryptoSeed.getBirthdate() == null ? System.currentTimeMillis() : cryptoSeed.getBirthdate().getTime());
|
||||
}
|
||||
|
||||
private DeterministicSeed getSeed(CryptoBip39 cryptoBip39) {
|
||||
return new DeterministicSeed(cryptoBip39.getWords(), null, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
|
||||
}
|
||||
}
|
||||
|
||||
private class QRScanListener implements WebcamListener {
|
||||
@@ -634,6 +658,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
public final ExtendedKey extendedKey;
|
||||
public final OutputDescriptor outputDescriptor;
|
||||
public final List<Wallet> wallets;
|
||||
public final DeterministicSeed seed;
|
||||
public final String payload;
|
||||
public final Throwable exception;
|
||||
|
||||
@@ -644,6 +669,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -655,6 +681,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -666,6 +693,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -677,6 +705,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -688,6 +717,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = extendedKey;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -699,6 +729,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -710,6 +741,19 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = wallets;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
|
||||
public Result(DeterministicSeed seed) {
|
||||
this.transaction = null;
|
||||
this.psbt = null;
|
||||
this.uri = null;
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = seed;
|
||||
this.payload = null;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -721,6 +765,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = payload;
|
||||
this.exception = null;
|
||||
}
|
||||
@@ -732,6 +777,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
this.extendedKey = null;
|
||||
this.outputDescriptor = null;
|
||||
this.wallets = null;
|
||||
this.seed = null;
|
||||
this.payload = null;
|
||||
this.exception = exception;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class SearchWalletDialog extends Dialog<Entry> {
|
||||
private static final Logger log = LoggerFactory.getLogger(SearchWalletDialog.class);
|
||||
|
||||
private final List<WalletForm> walletForms;
|
||||
private final TextField search;
|
||||
private final CoinTreeTable results;
|
||||
|
||||
public SearchWalletDialog(List<WalletForm> walletForms) {
|
||||
this.walletForms = walletForms;
|
||||
|
||||
if(walletForms.isEmpty()) {
|
||||
throw new IllegalArgumentException("No wallets selected to search");
|
||||
}
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("search.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText("Search Wallet");
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
if(!image.isError()) {
|
||||
ImageView imageView = new ImageView();
|
||||
imageView.setSmooth(false);
|
||||
imageView.setImage(image);
|
||||
dialogPane.setGraphic(imageView);
|
||||
}
|
||||
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(20);
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
fieldset.setText("");
|
||||
fieldset.setSpacing(10);
|
||||
|
||||
Field searchField = new Field();
|
||||
searchField.setText("Search:");
|
||||
search = TextFields.createClearableTextField();
|
||||
search.setPromptText("Label, address, value or transaction ID");
|
||||
searchField.getInputs().add(search);
|
||||
|
||||
fieldset.getChildren().addAll(searchField);
|
||||
form.getChildren().add(fieldset);
|
||||
|
||||
boolean showWallet = walletForms.size() > 1 || walletForms.stream().anyMatch(walletForm -> !walletForm.getNestedWalletForms().isEmpty());
|
||||
|
||||
results = new CoinTreeTable();
|
||||
results.setShowRoot(false);
|
||||
results.setPrefWidth(showWallet ? 950 : 850);
|
||||
results.setUnitFormat(walletForms.iterator().next().getWallet());
|
||||
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
results.setPlaceholder(new Label("No results"));
|
||||
|
||||
if(showWallet) {
|
||||
TreeTableColumn<Entry, String> walletColumn = new TreeTableColumn<>("Wallet");
|
||||
walletColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getWallet().getDisplayName());
|
||||
});
|
||||
results.getColumns().add(walletColumn);
|
||||
}
|
||||
|
||||
TreeTableColumn<Entry, String> typeColumn = new TreeTableColumn<>("Type");
|
||||
typeColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getEntryType());
|
||||
});
|
||||
results.getColumns().add(typeColumn);
|
||||
|
||||
TreeTableColumn<Entry, Entry> entryCol = new TreeTableColumn<>("Date / Address / Output");
|
||||
entryCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
|
||||
});
|
||||
entryCol.setCellFactory(p -> new SearchEntryCell());
|
||||
String address = walletForms.iterator().next().getNodeEntry(KeyPurpose.RECEIVE).getAddress().toString();
|
||||
entryCol.setMinWidth(TextUtils.computeTextWidth(AppServices.getMonospaceFont(), address, 0.0));
|
||||
results.getColumns().add(entryCol);
|
||||
|
||||
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
|
||||
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
return param.getValue().getValue().labelProperty();
|
||||
});
|
||||
labelCol.setCellFactory(p -> new SearchLabelCell());
|
||||
results.getColumns().add(labelCol);
|
||||
|
||||
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
|
||||
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
|
||||
});
|
||||
amountCol.setCellFactory(p -> new CoinCell());
|
||||
results.getColumns().add(amountCol);
|
||||
|
||||
vBox.getChildren().addAll(form, results);
|
||||
dialogPane.setContent(vBox);
|
||||
|
||||
ButtonType showButtonType = new javafx.scene.control.ButtonType("Show", ButtonBar.ButtonData.APPLY);
|
||||
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType, showButtonType);
|
||||
|
||||
Button showButton = (Button) dialogPane.lookupButton(showButtonType);
|
||||
showButton.setDefaultButton(true);
|
||||
showButton.setDisable(true);
|
||||
|
||||
setResultConverter(buttonType -> buttonType == showButtonType ? results.getSelectionModel().getSelectedItem().getValue() : null);
|
||||
|
||||
results.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> {
|
||||
showButton.setDisable(results.getSelectionModel().getSelectedCells().isEmpty()
|
||||
|| walletForms.stream().map(WalletForm::getWallet).noneMatch(wallet -> wallet == results.getSelectionModel().getSelectedItem().getValue().getWallet()));
|
||||
});
|
||||
|
||||
search.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
searchWallet(newValue.toLowerCase(Locale.ROOT));
|
||||
});
|
||||
|
||||
setResizable(true);
|
||||
|
||||
Platform.runLater(search::requestFocus);
|
||||
}
|
||||
|
||||
private void searchWallet(String searchText) {
|
||||
List<Entry> matchingEntries = new ArrayList<>();
|
||||
|
||||
if(!searchText.isEmpty()) {
|
||||
Long searchValue = null;
|
||||
|
||||
try {
|
||||
searchValue = Math.abs(Long.parseLong(searchText));
|
||||
} catch(NumberFormatException e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
for(WalletForm walletForm : walletForms) {
|
||||
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(WalletForm nestedWalletForm : walletForm.getNestedWalletForms()) {
|
||||
for(KeyPurpose keyPurpose : nestedWalletForm.getWallet().getWalletKeyPurposes()) {
|
||||
NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
if(hashIndexEntry.getBlockTransaction().getHash().toString().toLowerCase(Locale.ROOT).equals(searchText) ||
|
||||
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.iterator().next().getWallet(), matchingEntries);
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
results.setRoot(rootItem);
|
||||
}
|
||||
|
||||
private static class SearchWalletEntry extends Entry {
|
||||
public SearchWalletEntry(Wallet wallet, List<Entry> entries) {
|
||||
super(wallet, wallet.getName(), entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getValue() {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEntryType() {
|
||||
return "Search Wallet Results";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function getWalletFunction() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class SearchEntryCell extends EntryCell {
|
||||
@Override
|
||||
protected void updateItem(Entry entry, boolean empty) {
|
||||
super.updateItem(entry, empty);
|
||||
setContextMenu(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SearchLabelCell extends LabelCell {
|
||||
@Override
|
||||
public void updateItem(String label, boolean empty) {
|
||||
super.updateItem(label, empty);
|
||||
setContextMenu(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ public class SeedDisplayDialog extends Dialog<Void> {
|
||||
Accordion keystoreAccordion = new Accordion();
|
||||
scrollPane.setContent(keystoreAccordion);
|
||||
|
||||
MnemonicKeystoreImportPane keystorePane = new MnemonicKeystoreImportPane(decryptedKeystore);
|
||||
MnemonicKeystoreDisplayPane keystorePane = new MnemonicKeystoreDisplayPane(decryptedKeystore);
|
||||
keystorePane.setAnimated(false);
|
||||
keystoreAccordion.getPanes().add(keystorePane);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.StringConverter;
|
||||
@@ -47,7 +48,19 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
|
||||
Grid grid = getGrid(initialPayments);
|
||||
|
||||
spreadsheetView = new SpreadsheetView(grid);
|
||||
spreadsheetView = new SpreadsheetView(grid) {
|
||||
@Override
|
||||
public void pasteClipboard() {
|
||||
final Clipboard clipboard = Clipboard.getSystemClipboard();
|
||||
if(clipboard.hasString()) {
|
||||
final TablePosition<?,?> tp = getSelectionModel().getFocusedCell();
|
||||
SpreadsheetCell cell = getGrid().getRows().get(tp.getRow()).get(tp.getColumn());
|
||||
getGrid().setCellValue(cell.getRow(), cell.getColumn(), cell.getCellType().convertValue(clipboard.getString()));
|
||||
} else {
|
||||
super.pasteClipboard();
|
||||
}
|
||||
}
|
||||
};
|
||||
spreadsheetView.getColumns().get(0).setPrefWidth(400);
|
||||
spreadsheetView.getColumns().get(1).setPrefWidth(150);
|
||||
spreadsheetView.getColumns().get(2).setPrefWidth(247);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.scene.control.DialogPane;
|
||||
import javafx.scene.image.Image;
|
||||
@@ -22,4 +23,117 @@ public class ServiceProgressDialog extends ProgressDialog {
|
||||
Image image = new Image(imagePath);
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
}
|
||||
|
||||
public static class ProxyWorker implements Worker<Boolean> {
|
||||
private final ObjectProperty<State> state = new SimpleObjectProperty<>(this, "state", State.READY);
|
||||
private final StringProperty message = new SimpleStringProperty(this, "message", "");
|
||||
private final DoubleProperty progress = new SimpleDoubleProperty(this, "progress", -1);
|
||||
|
||||
public void start() {
|
||||
state.set(State.SCHEDULED);
|
||||
}
|
||||
|
||||
public void end() {
|
||||
state.set(State.SUCCEEDED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public State getState() {
|
||||
return state.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getValue() {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<Boolean> valueProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable getException() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<Throwable> exceptionProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getWorkDone() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyDoubleProperty workDoneProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getTotalWork() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyDoubleProperty totalWorkProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getProgress() {
|
||||
return progress.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyDoubleProperty progressProperty() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyBooleanProperty runningProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return message.get();
|
||||
}
|
||||
|
||||
public void setMessage(String strMessage) {
|
||||
message.set(strMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyStringProperty messageProperty() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTitle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyStringProperty titleProperty() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,4 +34,18 @@ public class TextUtils {
|
||||
helper.setText(DEFAULT_TEXT);
|
||||
return d;
|
||||
}
|
||||
|
||||
public static double computeTextHeight(Font font, String text) {
|
||||
helper.setText(text);
|
||||
helper.setFont(font);
|
||||
|
||||
helper.setWrappingWidth(0.0D);
|
||||
helper.setLineSpacing(0.0D);
|
||||
double d = Math.ceil(helper.getLayoutBounds().getHeight());
|
||||
|
||||
helper.setWrappingWidth(DEFAULT_WRAPPING_WIDTH);
|
||||
helper.setLineSpacing(DEFAULT_LINE_SPACING);
|
||||
helper.setText(DEFAULT_TEXT);
|
||||
return d;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,48 +6,139 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
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;
|
||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||
import com.sparrowwallet.sparrow.event.SorobanInitiatedEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
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;
|
||||
import javafx.util.Duration;
|
||||
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;
|
||||
|
||||
public class TransactionDiagram extends GridPane {
|
||||
private static final int MAX_UTXOS = 8;
|
||||
private static final int REDUCED_MAX_UTXOS = MAX_UTXOS - 2;
|
||||
private static final int EXPANDED_MAX_UTXOS = 20;
|
||||
private static final int MAX_PAYMENTS = 6;
|
||||
private static final int REDUCED_MAX_PAYMENTS = MAX_PAYMENTS - 2;
|
||||
private static final int EXPANDED_MAX_PAYMENTS = 18;
|
||||
private static final double DIAGRAM_HEIGHT = 210.0;
|
||||
private static final double REDUCED_DIAGRAM_HEIGHT = DIAGRAM_HEIGHT - 60;
|
||||
private static final double EXPANDED_DIAGRAM_HEIGHT = 500;
|
||||
private static final int TOOLTIP_SHOW_DELAY = 50;
|
||||
private static final int RELATIVE_SIZE_MAX_RADIUS = 7;
|
||||
private static final int ROW_HEIGHT = 27;
|
||||
|
||||
private WalletTransaction walletTx;
|
||||
private final BooleanProperty finalProperty = new SimpleBooleanProperty(false);
|
||||
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(getDiagramTitle());
|
||||
stage.initOwner(TransactionDiagram.this.getScene().getWindow());
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.setResizable(false);
|
||||
|
||||
StackPane scenePane = new StackPane();
|
||||
if(Platform.getCurrent() == Platform.WINDOWS) {
|
||||
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||
}
|
||||
|
||||
scenePane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
if(Config.get().getTheme() == Theme.DARK) {
|
||||
scenePane.getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||
}
|
||||
scenePane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||
scenePane.getStylesheets().add(AppServices.class.getResource("wallet/send.css").toExternalForm());
|
||||
|
||||
VBox vBox = new VBox(20);
|
||||
vBox.setPadding(new Insets(20, 40, 20, 50));
|
||||
|
||||
expandedDiagram = new TransactionDiagram();
|
||||
expandedDiagram.setId("transactionDiagram");
|
||||
expandedDiagram.setExpanded(true);
|
||||
expandedDiagram.setFinal(isFinal());
|
||||
updateDerivedDiagram(expandedDiagram);
|
||||
|
||||
HBox buttonBox = new HBox();
|
||||
buttonBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
Button button = new Button("Close");
|
||||
button.setOnAction(e -> {
|
||||
stage.close();
|
||||
});
|
||||
buttonBox.getChildren().add(button);
|
||||
vBox.getChildren().addAll(expandedDiagram, buttonBox);
|
||||
scenePane.getChildren().add(vBox);
|
||||
|
||||
Scene scene = new Scene(scenePane);
|
||||
AppServices.onEscapePressed(scene, stage::close);
|
||||
AppServices.setStageIcon(stage);
|
||||
stage.setScene(scene);
|
||||
stage.setOnShowing(e -> {
|
||||
AppServices.moveToActiveWindowScreen(stage, 600, 460);
|
||||
});
|
||||
stage.setOnHidden(e -> {
|
||||
expandedDiagram = null;
|
||||
});
|
||||
stage.show();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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());
|
||||
@@ -58,6 +149,10 @@ public class TransactionDiagram extends GridPane {
|
||||
} else {
|
||||
this.walletTx = walletTx;
|
||||
update();
|
||||
setOnMouseClicked(expandedDiagramHandler);
|
||||
if(expandedDiagram != null) {
|
||||
updateDerivedDiagram(expandedDiagram);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +180,26 @@ public class TransactionDiagram extends GridPane {
|
||||
getChildren().clear();
|
||||
}
|
||||
|
||||
private void updateDerivedDiagram(TransactionDiagram diagram) {
|
||||
diagram.setOptimizationStrategy(getOptimizationStrategy());
|
||||
diagram.walletTx = walletTx;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
diagram.update();
|
||||
|
||||
if(diagram.getScene() != null && diagram.getScene().getWindow() instanceof Stage stage) {
|
||||
stage.sizeToScene();
|
||||
}
|
||||
}
|
||||
|
||||
public void update() {
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = getDisplayedUtxoSets();
|
||||
|
||||
@@ -110,12 +225,23 @@ 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() {
|
||||
boolean addUserSet = getOptimizationStrategy() == OptimizationStrategy.PRIVACY && SorobanServices.canWalletMix(walletTx.getWallet())
|
||||
&& walletTx.getPayments().size() == 1
|
||||
&& (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getAddress(walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE)).getScriptType());
|
||||
&& (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>();
|
||||
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) {
|
||||
@@ -285,17 +411,21 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
private Pane getInputsLabels(List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets) {
|
||||
VBox inputsBox = new VBox();
|
||||
inputsBox.setMaxWidth(150);
|
||||
inputsBox.setPrefWidth(150);
|
||||
inputsBox.setMaxWidth(isExpanded() ? 300 : 150);
|
||||
inputsBox.setPrefWidth(isExpanded() ? 230 : 150);
|
||||
inputsBox.setPadding(new Insets(0, 10, 0, 10));
|
||||
inputsBox.minHeightProperty().bind(minHeightProperty());
|
||||
inputsBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
inputsBox.setAlignment(Pos.BASELINE_RIGHT);
|
||||
inputsBox.getChildren().add(createSpacer());
|
||||
Label helper = new Label();
|
||||
double labelHeight = Math.max(TextUtils.computeTextHeight(AppServices.getMonospaceFont(), "0"), TextUtils.computeTextHeight(helper.getFont(), "0")) + 1;
|
||||
for(Map<BlockTransactionHashIndex, WalletNode> displayedUtxos : displayedUtxoSets) {
|
||||
for(BlockTransactionHashIndex input : displayedUtxos.keySet()) {
|
||||
WalletNode walletNode = displayedUtxos.get(input);
|
||||
String desc = getInputDescription(input);
|
||||
Label label = new Label(desc);
|
||||
label.setPrefHeight(labelHeight);
|
||||
label.setAlignment(Pos.BASELINE_LEFT);
|
||||
label.getStyleClass().add("utxo-label");
|
||||
|
||||
Button excludeUtxoButton = new Button("");
|
||||
@@ -305,8 +435,12 @@ public class TransactionDiagram extends GridPane {
|
||||
});
|
||||
|
||||
Tooltip tooltip = new Tooltip();
|
||||
Long inputValue = null;
|
||||
if(walletNode != null) {
|
||||
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + (isFinal() ? walletTx.getWallet().getFullDisplayName() : "") + " " + walletNode + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
|
||||
inputValue = input.getValue();
|
||||
Wallet nodeWallet = walletNode.getWallet();
|
||||
tooltip.setText("Spending " + getSatsValue(inputValue) + " sats from " + (isFinal() ? nodeWallet.getFullDisplayName() : (nodeWallet.isNested() ? nodeWallet.getDisplayName() : "")) + " " + walletNode + "\n" +
|
||||
input.getHashAsString() + ":" + input.getIndex() + "\n" + walletNode.getAddress());
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
|
||||
if(input.getLabel() == null || input.getLabel().isEmpty()) {
|
||||
@@ -317,11 +451,16 @@ public class TransactionDiagram extends GridPane {
|
||||
label.setGraphic(excludeUtxoButton);
|
||||
label.setContentDisplay(ContentDisplay.LEFT);
|
||||
}
|
||||
|
||||
ContextMenu contextMenu = new LabelContextMenu(walletNode.getAddress(), inputValue);
|
||||
label.setContextMenu(contextMenu);
|
||||
} else {
|
||||
if(input instanceof PayjoinBlockTransactionHashIndex) {
|
||||
tooltip.setText("Added once transaction is signed and sent to the payjoin server");
|
||||
} else if(input instanceof AdditionalBlockTransactionHashIndex additionalReference) {
|
||||
inputValue = input.getValue();
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
joiner.add("Spending " + getSatsValue(inputValue) + " sats from" + (isExpanded() ? ":" : " (click to expand):"));
|
||||
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
|
||||
joiner.add(getInputDescription(additionalInput));
|
||||
}
|
||||
@@ -333,18 +472,29 @@ public class TransactionDiagram extends GridPane {
|
||||
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getWarningGlyph());
|
||||
label.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
} else {
|
||||
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
|
||||
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int) input.getIndex());
|
||||
Address fromAddress = txOutput.getScript().getToAddress();
|
||||
tooltip.setText("Input of " + getSatsValue(txOutput.getValue()) + " sats\n" + input.getHashAsString() + ":" + input.getIndex() + (fromAddress != null ? "\n" + fromAddress : ""));
|
||||
inputValue = txOutput.getValue();
|
||||
tooltip.setText("Input of " + getSatsValue(inputValue) + " sats\n" + input.getHashAsString() + ":" + input.getIndex() + (fromAddress != null ? "\n" + fromAddress : ""));
|
||||
|
||||
ContextMenu contextMenu = new LabelContextMenu(fromAddress, inputValue);
|
||||
label.setContextMenu(contextMenu);
|
||||
} else {
|
||||
tooltip.setText(input.getHashAsString() + ":" + input.getIndex());
|
||||
}
|
||||
label.getStyleClass().add("input-label");
|
||||
}
|
||||
if(!isFinal()) {
|
||||
label.setGraphic(excludeUtxoButton);
|
||||
label.setContentDisplay(ContentDisplay.LEFT);
|
||||
excludeUtxoButton.setVisible(false);
|
||||
}
|
||||
tooltip.getStyleClass().add("input-label");
|
||||
}
|
||||
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
@@ -353,7 +503,26 @@ public class TransactionDiagram extends GridPane {
|
||||
label.setTooltip(tooltip);
|
||||
}
|
||||
|
||||
inputsBox.getChildren().add(label);
|
||||
HBox inputBox = new HBox();
|
||||
inputBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
inputBox.getChildren().add(label);
|
||||
|
||||
if(isExpanded()) {
|
||||
label.setMinWidth(120);
|
||||
Region region = new Region();
|
||||
HBox.setHgrow(region, Priority.ALWAYS);
|
||||
CoinLabel amountLabel = new CoinLabel();
|
||||
if(inputValue != null) {
|
||||
amountLabel.setValue(inputValue);
|
||||
} else if(label.getText().trim().isEmpty()) {
|
||||
amountLabel.setText("");
|
||||
}
|
||||
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 12);
|
||||
amountLabel.setPadding(new Insets(0, 0, 0, 10));
|
||||
inputBox.getChildren().addAll(region, amountLabel);
|
||||
}
|
||||
|
||||
inputsBox.getChildren().add(inputBox);
|
||||
inputsBox.getChildren().add(createSpacer());
|
||||
}
|
||||
}
|
||||
@@ -387,6 +556,7 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
double width = 140.0;
|
||||
List<BlockTransactionHashIndex> inputs = new ArrayList<>(displayedUtxos.keySet());
|
||||
long sum = walletTx.getTotal();
|
||||
int numUtxos = displayedUtxos.size();
|
||||
for(int i = 1; i <= numUtxos; i++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
@@ -398,7 +568,7 @@ public class TransactionDiagram extends GridPane {
|
||||
continue;
|
||||
}
|
||||
|
||||
curve.setStartX(0);
|
||||
curve.setStartX(RELATIVE_SIZE_MAX_RADIUS);
|
||||
double scaleFactor = (double)i / (numUtxos + 1);
|
||||
int nodeHeight = 17;
|
||||
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
|
||||
@@ -406,12 +576,19 @@ public class TransactionDiagram extends GridPane {
|
||||
curve.setEndX(width);
|
||||
curve.setEndY(scale(getDiagramHeight(), 0.5, 0));
|
||||
|
||||
curve.setControlX1(scale(width, 0.2, 0));
|
||||
curve.setControlX1(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.2, 0));
|
||||
curve.setControlY1(curve.getStartY());
|
||||
curve.setControlX2(scale(width, 0.8, 0));
|
||||
curve.setControlX2(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.8, 0));
|
||||
curve.setControlY2(curve.getEndY());
|
||||
|
||||
group.getChildren().add(curve);
|
||||
|
||||
if(sum > 0 && !curve.getStyleClass().contains("input-dashed-line")) {
|
||||
long radius = Math.round((double)inputs.get(numUtxos-i).getValue() * (RELATIVE_SIZE_MAX_RADIUS - 1) / sum) + 1;
|
||||
Circle circle = new Circle(curve.getStartX(), curve.getStartY(), radius);
|
||||
circle.getStyleClass().add("size-indicator");
|
||||
group.getChildren().add(circle);
|
||||
}
|
||||
}
|
||||
|
||||
pane.getChildren().add(group);
|
||||
@@ -458,6 +635,9 @@ public class TransactionDiagram extends GridPane {
|
||||
group.getChildren().add(yaxisLine);
|
||||
|
||||
double width = 140.0;
|
||||
long sum = walletTx.getTotal();
|
||||
List<Long> values = walletTx.getTransaction().getOutputs().stream().map(TransactionOutput::getValue).collect(Collectors.toList());
|
||||
values.add(walletTx.getFee());
|
||||
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
||||
for(int i = 1; i <= numOutputs; i++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
@@ -465,18 +645,25 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
curve.setStartX(0);
|
||||
curve.setStartY(scale(getDiagramHeight(), 0.5, 0));
|
||||
curve.setEndX(width);
|
||||
curve.setEndX(width - RELATIVE_SIZE_MAX_RADIUS);
|
||||
double scaleFactor = (double)i / (numOutputs + 1);
|
||||
int nodeHeight = 20;
|
||||
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
|
||||
curve.setEndY(scale(getDiagramHeight(), scaleFactor, additional));
|
||||
|
||||
curve.setControlX1(scale(width, 0.2, 0));
|
||||
curve.setControlX1(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.2, 0));
|
||||
curve.controlY1Property().bind(curve.startYProperty());
|
||||
curve.setControlX2(scale(width, 0.8, 0));
|
||||
curve.setControlX2(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.8, 0));
|
||||
curve.controlY2Property().bind(curve.endYProperty());
|
||||
|
||||
group.getChildren().add(curve);
|
||||
|
||||
if(sum > 0) {
|
||||
long radius = Math.min(RELATIVE_SIZE_MAX_RADIUS, Math.round((double)values.get(numOutputs-i) * (RELATIVE_SIZE_MAX_RADIUS - 1) / sum) + 1);
|
||||
Circle circle = new Circle(curve.getEndX(), curve.getEndY(), radius);
|
||||
circle.getStyleClass().add("size-indicator");
|
||||
group.getChildren().add(circle);
|
||||
}
|
||||
}
|
||||
|
||||
pane.getChildren().add(group);
|
||||
@@ -485,9 +672,10 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
private Pane getOutputsLabels(List<Payment> displayedPayments) {
|
||||
VBox outputsBox = new VBox();
|
||||
outputsBox.setMaxWidth(150);
|
||||
outputsBox.setMaxWidth(isExpanded() ? 350 : 150);
|
||||
outputsBox.setPrefWidth(isExpanded() ? 230 : 150);
|
||||
outputsBox.setPadding(new Insets(0, 20, 0, 10));
|
||||
outputsBox.setAlignment(Pos.CENTER_LEFT);
|
||||
outputsBox.setAlignment(Pos.BASELINE_LEFT);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
List<OutputNode> outputNodes = new ArrayList<>();
|
||||
@@ -499,15 +687,30 @@ public class TransactionDiagram extends GridPane {
|
||||
recipientLabel.getStyleClass().add("output-label");
|
||||
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
|
||||
Wallet toWallet = getToWallet(payment);
|
||||
WalletNode toNode = walletTx.getWallet() != null ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null;
|
||||
WalletNode toNode = walletTx.getWallet() != null && !walletTx.getWallet().isBip47() ? walletTx.getAddressNodeMap().get(payment.getAddress()) : null;
|
||||
Wallet toBip47Wallet = getBip47SendWallet(payment);
|
||||
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
|
||||
+ getSatsValue(payment.getAmount()) + " sats to "
|
||||
+ (payment instanceof AdditionalPayment ? "\n" + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : "external address") : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString()));
|
||||
+ (payment instanceof AdditionalPayment ? (isExpanded() ? "\n" : "(click to expand)\n") + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : (toBip47Wallet == null ? "external address" : toBip47Wallet.getDisplayName())) : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString()));
|
||||
recipientTooltip.getStyleClass().add("recipient-label");
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
recipientLabel.setTooltip(recipientTooltip);
|
||||
outputNodes.add(new OutputNode(recipientLabel, payment.getAddress()));
|
||||
HBox paymentBox = new HBox();
|
||||
paymentBox.setAlignment(Pos.CENTER_LEFT);
|
||||
paymentBox.getChildren().add(recipientLabel);
|
||||
if(isExpanded()) {
|
||||
recipientLabel.setMinWidth(120);
|
||||
Region region = new Region();
|
||||
region.setMinWidth(20);
|
||||
HBox.setHgrow(region, Priority.ALWAYS);
|
||||
CoinLabel amountLabel = new CoinLabel();
|
||||
amountLabel.setValue(payment.getAmount());
|
||||
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
|
||||
paymentBox.getChildren().addAll(region, amountLabel);
|
||||
}
|
||||
|
||||
outputNodes.add(new OutputNode(paymentBox, payment.getAddress(), payment.getAmount()));
|
||||
}
|
||||
|
||||
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
|
||||
@@ -516,6 +719,7 @@ public class TransactionDiagram extends GridPane {
|
||||
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.setAlignment(Pos.CENTER_LEFT);
|
||||
Address changeAddress = walletTx.getChangeAddress(changeNode);
|
||||
String changeDesc = changeAddress.toString().substring(0, 8) + "...";
|
||||
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
|
||||
@@ -543,7 +747,18 @@ public class TransactionDiagram extends GridPane {
|
||||
actionBox.getChildren().add(replaceChangeLabel);
|
||||
}
|
||||
|
||||
outputNodes.add(new OutputNode(actionBox, changeAddress));
|
||||
if(isExpanded()) {
|
||||
changeLabel.setMinWidth(120);
|
||||
Region region = new Region();
|
||||
region.setMinWidth(20);
|
||||
HBox.setHgrow(region, Priority.ALWAYS);
|
||||
CoinLabel amountLabel = new CoinLabel();
|
||||
amountLabel.setValue(changeEntry.getValue());
|
||||
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
|
||||
actionBox.getChildren().addAll(region, amountLabel);
|
||||
}
|
||||
|
||||
outputNodes.add(new OutputNode(actionBox, changeAddress, changeEntry.getValue()));
|
||||
}
|
||||
|
||||
if(isFinal()) {
|
||||
@@ -553,6 +768,11 @@ public class TransactionDiagram extends GridPane {
|
||||
for(OutputNode outputNode : outputNodes) {
|
||||
outputsBox.getChildren().add(outputNode.outputLabel);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
ContextMenu contextMenu = new LabelContextMenu(outputNode.address, outputNode.amount);
|
||||
if(!outputNode.outputLabel.getChildren().isEmpty() && outputNode.outputLabel.getChildren().get(0) instanceof Label outputLabelControl) {
|
||||
outputLabelControl.setContextMenu(contextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
boolean highFee = (walletTx.getFeePercentage() > 0.1);
|
||||
@@ -564,9 +784,28 @@ public class TransactionDiagram extends GridPane {
|
||||
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
feeTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
feeLabel.setTooltip(feeTooltip);
|
||||
outputsBox.getChildren().add(feeLabel);
|
||||
|
||||
HBox feeBox = new HBox();
|
||||
feeBox.setAlignment(Pos.CENTER_LEFT);
|
||||
feeBox.getChildren().add(feeLabel);
|
||||
if(isExpanded()) {
|
||||
Region region = new Region();
|
||||
region.setMinWidth(20);
|
||||
HBox.setHgrow(region, Priority.ALWAYS);
|
||||
CoinLabel amountLabel = new CoinLabel();
|
||||
amountLabel.setValue(walletTx.getFee());
|
||||
amountLabel.setMinWidth(TextUtils.computeTextWidth(amountLabel.getFont(), amountLabel.getText(), 0.0D) + 2);
|
||||
feeBox.getChildren().addAll(region, amountLabel);
|
||||
}
|
||||
|
||||
outputsBox.getChildren().add(feeBox);
|
||||
outputsBox.getChildren().add(createSpacer());
|
||||
|
||||
if(walletTx.getFee() >= 0) {
|
||||
ContextMenu contextMenu = new LabelContextMenu(null, walletTx.getFee());
|
||||
feeLabel.setContextMenu(contextMenu);
|
||||
}
|
||||
|
||||
return outputsBox;
|
||||
}
|
||||
|
||||
@@ -586,13 +825,56 @@ 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();
|
||||
}
|
||||
|
||||
if(isReducedHeight()) {
|
||||
return REDUCED_DIAGRAM_HEIGHT;
|
||||
}
|
||||
@@ -601,6 +883,10 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
private int getMaxUtxos() {
|
||||
if(isExpanded()) {
|
||||
return EXPANDED_MAX_UTXOS;
|
||||
}
|
||||
|
||||
if(isReducedHeight()) {
|
||||
return REDUCED_MAX_UTXOS;
|
||||
}
|
||||
@@ -609,6 +895,10 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
private int getMaxPayments() {
|
||||
if(isExpanded()) {
|
||||
return EXPANDED_MAX_PAYMENTS;
|
||||
}
|
||||
|
||||
if(isReducedHeight()) {
|
||||
return REDUCED_MAX_PAYMENTS;
|
||||
}
|
||||
@@ -634,7 +924,7 @@ public class TransactionDiagram extends GridPane {
|
||||
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
|
||||
return "Whirlpool Fee";
|
||||
} else if(walletTx.isPremixSend(payment)) {
|
||||
int premixIndex = getOutputIndex(payment.getAddress()) - 2;
|
||||
int premixIndex = getOutputIndex(payment.getAddress(), payment.getAmount()) - 2;
|
||||
return "Premix #" + premixIndex;
|
||||
} else if(walletTx.isBadbankSend(payment)) {
|
||||
return "Badbank Change";
|
||||
@@ -643,14 +933,34 @@ public class TransactionDiagram extends GridPane {
|
||||
return null;
|
||||
}
|
||||
|
||||
private int getOutputIndex(Address address) {
|
||||
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress())).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
|
||||
private int getOutputIndex(Address address, long amount) {
|
||||
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private Wallet getToWallet(Payment payment) {
|
||||
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
|
||||
if(openWallet != walletTx.getWallet() && openWallet.isValid() && openWallet.isWalletAddress(payment.getAddress())) {
|
||||
return openWallet;
|
||||
if(openWallet != walletTx.getWallet() && openWallet.isValid()) {
|
||||
WalletNode addressNode = openWallet.getWalletAddresses().get(payment.getAddress());
|
||||
if(addressNode != null) {
|
||||
return addressNode.getWallet();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Wallet getBip47SendWallet(Payment payment) {
|
||||
if(walletTx.getWallet() != null) {
|
||||
for(Wallet childWallet : walletTx.getWallet().getChildWallets()) {
|
||||
if(childWallet.isNested()) {
|
||||
WalletNode sendNode = childWallet.getNode(KeyPurpose.SEND);
|
||||
for(WalletNode sendAddressNode : sendNode.getChildren()) {
|
||||
if(sendAddressNode.getAddress().equals(payment.getAddress())) {
|
||||
return childWallet;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -814,6 +1124,8 @@ public class TransactionDiagram extends GridPane {
|
||||
});
|
||||
userAddGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
return userAddGlyph;
|
||||
}
|
||||
@@ -833,6 +1145,8 @@ public class TransactionDiagram extends GridPane {
|
||||
});
|
||||
coinsGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
} else {
|
||||
coinsGlyph.getStyleClass().add("coins-icon");
|
||||
@@ -841,6 +1155,12 @@ public class TransactionDiagram extends GridPane {
|
||||
return coinsGlyph;
|
||||
}
|
||||
|
||||
private void closeExpanded() {
|
||||
if(isExpanded() && this.getScene() != null && this.getScene().getWindow() instanceof Stage stage) {
|
||||
stage.close();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isFinal() {
|
||||
return finalProperty.get();
|
||||
}
|
||||
@@ -865,6 +1185,14 @@ public class TransactionDiagram extends GridPane {
|
||||
this.optimizationStrategyProperty.set(optimizationStrategy);
|
||||
}
|
||||
|
||||
public boolean isExpanded() {
|
||||
return expanded;
|
||||
}
|
||||
|
||||
public void setExpanded(boolean expanded) {
|
||||
this.expanded = expanded;
|
||||
}
|
||||
|
||||
private static class PayjoinBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
public PayjoinBlockTransactionHashIndex() {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
@@ -880,7 +1208,7 @@ public class TransactionDiagram extends GridPane {
|
||||
private final List<BlockTransactionHashIndex> additionalInputs;
|
||||
|
||||
public AdditionalBlockTransactionHashIndex(List<BlockTransactionHashIndex> additionalInputs) {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, additionalInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum());
|
||||
this.additionalInputs = additionalInputs;
|
||||
}
|
||||
|
||||
@@ -889,6 +1217,11 @@ public class TransactionDiagram extends GridPane {
|
||||
return additionalInputs.size() + " more...";
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getValue() {
|
||||
return additionalInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum();
|
||||
}
|
||||
|
||||
public List<BlockTransactionHashIndex> getAdditionalInputs() {
|
||||
return additionalInputs;
|
||||
}
|
||||
@@ -947,21 +1280,63 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
private class OutputNode implements Comparable<OutputNode> {
|
||||
public Node outputLabel;
|
||||
public Pane outputLabel;
|
||||
public Address address;
|
||||
public long amount;
|
||||
|
||||
public OutputNode(Node outputLabel, Address address) {
|
||||
public OutputNode(Pane outputLabel, Address address, long amount) {
|
||||
this.outputLabel = outputLabel;
|
||||
this.address = address;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(TransactionDiagram.OutputNode o) {
|
||||
try {
|
||||
return getOutputIndex(address) - getOutputIndex(o.address);
|
||||
return getOutputIndex(address, amount) - getOutputIndex(o.address, o.amount);
|
||||
} catch(Exception e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class LabelContextMenu extends ContextMenu {
|
||||
public LabelContextMenu(Address address, long value) {
|
||||
if(address != null) {
|
||||
MenuItem copyAddress = new MenuItem("Copy Address");
|
||||
copyAddress.setOnAction(event -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(address.toString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyAddress);
|
||||
|
||||
MenuItem 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");
|
||||
copySatsValue.setOnAction(event -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(Long.toString(value));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
MenuItem copyBtcValue = new MenuItem("Copy Value in BTC");
|
||||
copyBtcValue.setOnAction(event -> {
|
||||
hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
content.putString(format.formatBtcValue(value));
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().addAll(copySatsValue, copyBtcValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,8 +165,9 @@ public class TransactionHexArea extends CodeArea {
|
||||
for(int j = 0; j < witness.getPushes().size(); j++) {
|
||||
byte[] push = witness.getPushes().get(j);
|
||||
VarInt witnessLen = new VarInt(push.length);
|
||||
boolean isSignature = isSignature(push);
|
||||
cursor = addSegment(segments, cursor, witnessLen.getSizeInBytes() * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "length"));
|
||||
cursor = addSegment(segments, cursor, (int) witnessLen.value * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "data"));
|
||||
cursor = addSegment(segments, cursor, (int) witnessLen.value * 2, i, j, "witness-" + getIndexedStyleClass(i, selectedInputIndex, "data" + (isSignature ? "-signature" : "")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,6 +205,19 @@ public class TransactionHexArea extends CodeArea {
|
||||
return "other";
|
||||
}
|
||||
|
||||
private boolean isSignature(byte[] data) {
|
||||
if(data.length >= 64) {
|
||||
try {
|
||||
TransactionSignature.decodeFromBitcoin(data, false);
|
||||
return true;
|
||||
} catch(Exception e) {
|
||||
//ignore, not a signature
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private String describeTransactionPart(Collection<String> styles) {
|
||||
String style = "";
|
||||
Integer index = null;
|
||||
@@ -238,7 +252,7 @@ public class TransactionHexArea extends CodeArea {
|
||||
case "output-pubkeyscript" -> "Output #" + index + " scriptPubKey";
|
||||
case "witness-count" -> "Input #" + index + " witness count";
|
||||
case "witness-length" -> "Input #" + index + " witness #" + witnessIndex + " length";
|
||||
case "witness-data" -> "Input #" + index + " witness #" + witnessIndex + " data";
|
||||
case "witness-data", "witness-data-signature" -> "Input #" + index + " witness #" + witnessIndex + " data";
|
||||
case "locktime" -> "Locktime";
|
||||
default -> "";
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
@@ -71,7 +68,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
|
||||
}
|
||||
}
|
||||
|
||||
public void updateHistory(List<WalletNode> updatedNodes) {
|
||||
public void updateHistory() {
|
||||
//Transaction entries should have already been updated using WalletTransactionsEntry.updateHistory, so only a resort required
|
||||
sort();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -18,7 +19,7 @@ import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class UtxosChart extends BarChart<String, Number> {
|
||||
private static final int MAX_BARS = 8;
|
||||
private static final int MAX_BARS = 10;
|
||||
private static final String OTHER_CATEGORY = "Other";
|
||||
private static final int TOOLTIP_SHOW_DELAY = 50;
|
||||
|
||||
@@ -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) {
|
||||
@@ -57,6 +57,7 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
|
||||
for(int i = 0; i < utxoDataList.size(); i++) {
|
||||
XYChart.Data<String, Number> newData = utxoDataList.get(i);
|
||||
newData.setXValue((i+1) + ". " + newData.getXValue());
|
||||
if(i < utxoSeries.getData().size()) {
|
||||
XYChart.Data<String, Number> existingData = utxoSeries.getData().get(i);
|
||||
if(!newData.getXValue().equals(existingData.getXValue()) || !newData.getYValue().equals(existingData.getYValue()) || (newData.getExtraValue() instanceof Entry && !newData.getExtraValue().equals(existingData.getExtraValue()))) {
|
||||
@@ -70,7 +71,7 @@ public class UtxosChart extends BarChart<String, Number> {
|
||||
}
|
||||
|
||||
if(utxoSeries.getData().size() > utxoDataList.size()) {
|
||||
utxoSeries.getData().remove(Math.max(0, utxoDataList.size() - 1), utxoSeries.getData().size());
|
||||
utxoSeries.getData().remove(utxoDataList.size(), utxoSeries.getData().size());
|
||||
}
|
||||
|
||||
if(selectedEntries != null) {
|
||||
@@ -116,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,6 +1,5 @@
|
||||
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;
|
||||
@@ -8,12 +7,11 @@ 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);
|
||||
@@ -24,6 +22,7 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
});
|
||||
dateCol.setCellFactory(p -> new DateCell());
|
||||
dateCol.setSortable(true);
|
||||
dateCol.setComparator(dateCol.getComparator().reversed());
|
||||
getColumns().add(dateCol);
|
||||
|
||||
TreeTableColumn<Entry, Entry> outputCol = new TreeTableColumn<>("Output");
|
||||
@@ -35,7 +34,12 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
outputCol.setComparator((o1, o2) -> {
|
||||
UtxoEntry entry1 = (UtxoEntry)o1;
|
||||
UtxoEntry entry2 = (UtxoEntry)o2;
|
||||
return entry1.getDescription().compareTo(entry2.getDescription());
|
||||
int hashCompare = entry1.getHashIndex().getHash().toString().compareTo(entry2.getHashIndex().getHash().toString());
|
||||
if(hashCompare != 0) {
|
||||
return hashCompare;
|
||||
}
|
||||
|
||||
return (int)(entry1.getHashIndex().getIndex() - entry2.getHashIndex().getIndex());
|
||||
});
|
||||
getColumns().add(outputCol);
|
||||
|
||||
@@ -86,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);
|
||||
@@ -99,7 +103,7 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
}
|
||||
}
|
||||
|
||||
public void updateHistory(List<WalletNode> updatedNodes) {
|
||||
public void updateHistory() {
|
||||
//Utxo entries should have already been updated, so only a resort required
|
||||
if(!getRoot().getChildren().isEmpty()) {
|
||||
sort();
|
||||
|
||||
@@ -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());
|
||||
} 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 CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(), new Descriptor(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||
}
|
||||
|
||||
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()));
|
||||
|
||||
@@ -31,6 +31,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
});
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
@@ -40,7 +41,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
stackPane.getChildren().add(anchorPane);
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
scrollPane.setPrefHeight(420);
|
||||
scrollPane.setPrefHeight(520);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
anchorPane.getChildren().add(scrollPane);
|
||||
scrollPane.setFitToWidth(true);
|
||||
@@ -48,19 +49,27 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
importAccordion = new Accordion();
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SpecterDIY());
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new 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());
|
||||
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()));
|
||||
|
||||
MnemonicWalletKeystoreImportPane mnemonicImportPane = new MnemonicWalletKeystoreImportPane(new Bip39());
|
||||
importAccordion.getPanes().add(0, mnemonicImportPane);
|
||||
|
||||
scrollPane.setContent(importAccordion);
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
@@ -75,7 +84,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
});
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(500);
|
||||
dialogPane.setPrefHeight(600);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton != cancelButtonType ? wallet : null);
|
||||
|
||||
@@ -10,19 +10,28 @@ import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class WalletLabelDialog extends Dialog<String> {
|
||||
private static final int MAX_LABEL_LENGTH = 25;
|
||||
|
||||
private final CustomTextField label;
|
||||
|
||||
public WalletLabelDialog(String initialName) {
|
||||
this(initialName, "Account");
|
||||
}
|
||||
|
||||
public WalletLabelDialog(String initialName, String walletType) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
setTitle("Account Name");
|
||||
dialogPane.setHeaderText("Enter a name for this account:");
|
||||
setTitle(walletType + " Name");
|
||||
dialogPane.setHeaderText("Enter a name for this " + walletType.toLowerCase(Locale.ROOT) + ":");
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
|
||||
dialogPane.setPrefWidth(400);
|
||||
@@ -48,17 +57,18 @@ public class WalletLabelDialog extends Dialog<String> {
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(label, Validator.combine(
|
||||
Validator.createEmptyValidator("Account name is required")
|
||||
Validator.createEmptyValidator(walletType + " name is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Label too long", newValue != null && newValue.length() > MAX_LABEL_LENGTH)
|
||||
));
|
||||
});
|
||||
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rename Account", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rename " + walletType, ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button)dialogPane.lookupButton(okButtonType);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> label.getText().length() == 0, label.textProperty());
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> label.getText().length() == 0 || label.getText().length() > MAX_LABEL_LENGTH, label.textProperty());
|
||||
okButton.disableProperty().bind(isInvalid);
|
||||
|
||||
label.setPromptText("Account Name");
|
||||
label.setPromptText(walletType + " Name");
|
||||
Platform.runLater(label::requestFocus);
|
||||
setResultConverter(dialogButton -> dialogButton == okButtonType ? label.getText() : null);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public class WalletPasswordDialog extends Dialog<SecureString> {
|
||||
dialogPane.getButtonTypes().addAll(okButtonType);
|
||||
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
|
||||
okButton.setPrefWidth(130);
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> passwordConfirm.isVisible() && !password.getText().equals(passwordConfirm.getText()), password.textProperty(), passwordConfirm.textProperty());
|
||||
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> (requirement == PasswordRequirement.LOAD && password.getText().isEmpty()) || (passwordConfirm.isVisible() && !password.getText().equals(passwordConfirm.getText())), password.textProperty(), passwordConfirm.textProperty());
|
||||
okButton.disableProperty().bind(isInvalid);
|
||||
|
||||
if(requirement != PasswordRequirement.UPDATE_NEW && requirement != PasswordRequirement.UPDATE_CHANGE) {
|
||||
|
||||
@@ -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,27 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
|
||||
public class ChildWalletAddedEvent extends WalletChangedEvent {
|
||||
private final Storage storage;
|
||||
private final Wallet childWallet;
|
||||
|
||||
public ChildWalletAddedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) {
|
||||
super(masterWallet);
|
||||
this.storage = storage;
|
||||
this.childWallet = childWallet;
|
||||
}
|
||||
|
||||
public Storage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
public Wallet getChildWallet() {
|
||||
return childWallet;
|
||||
}
|
||||
|
||||
public String getMasterWalletId() {
|
||||
return storage.getWalletId(getWallet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ChildWalletsAddedEvent extends WalletChangedEvent {
|
||||
private final Storage storage;
|
||||
private final List<Wallet> childWallets;
|
||||
|
||||
public ChildWalletsAddedEvent(Storage storage, Wallet masterWallet, Wallet childWallet) {
|
||||
super(masterWallet);
|
||||
this.storage = storage;
|
||||
this.childWallets = List.of(childWallet);
|
||||
}
|
||||
|
||||
public ChildWalletsAddedEvent(Storage storage, Wallet masterWallet, List<Wallet> childWallets) {
|
||||
super(masterWallet);
|
||||
this.storage = storage;
|
||||
this.childWallets = childWallets;
|
||||
}
|
||||
|
||||
public Storage getStorage() {
|
||||
return storage;
|
||||
}
|
||||
|
||||
public List<Wallet> getChildWallets() {
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
public String getMasterWalletId() {
|
||||
return storage.getWalletId(getWallet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.wallet.Function;
|
||||
|
||||
public class FunctionActionEvent {
|
||||
private final Function function;
|
||||
private final Wallet wallet;
|
||||
|
||||
public FunctionActionEvent(Function function, Wallet wallet) {
|
||||
this.function = function;
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Function getFunction() {
|
||||
return function;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public boolean selectFunction() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.control.CoinLabel;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
|
||||
@@ -13,7 +12,6 @@ import com.sparrowwallet.sparrow.wallet.TransactionHashIndexEntry;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class NewWalletTransactionsEvent {
|
||||
@@ -50,16 +48,21 @@ public class NewWalletTransactionsEvent {
|
||||
}
|
||||
|
||||
public String getValueAsText(long value) {
|
||||
UnitFormat format = Config.get().getUnitFormat();
|
||||
if(format == null) {
|
||||
format = UnitFormat.DOT;
|
||||
}
|
||||
|
||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
|
||||
}
|
||||
|
||||
if(unit == BitcoinUnit.BTC) {
|
||||
return CoinLabel.getBTCFormat().format((double) value / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
|
||||
return format.formatBtcValue(value) + " BTC";
|
||||
}
|
||||
|
||||
return String.format(Locale.ENGLISH, "%,d", value) + " sats";
|
||||
return format.formatSatsValue(value) + " sats";
|
||||
}
|
||||
|
||||
public List<BlockTransaction> getUnspentConfirmingWhirlpoolMixTransactions() {
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.wallet.Function;
|
||||
import com.sparrowwallet.sparrow.wallet.NodeEntry;
|
||||
|
||||
public class ReceiveActionEvent {
|
||||
private final Wallet wallet;
|
||||
|
||||
public class ReceiveActionEvent extends FunctionActionEvent {
|
||||
public ReceiveActionEvent(NodeEntry receiveEntry) {
|
||||
this.wallet = receiveEntry.getWallet();
|
||||
super(Function.RECEIVE, receiveEntry.getWallet());
|
||||
}
|
||||
|
||||
public ReceiveActionEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
super(Function.RECEIVE, wallet);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.net.Aopp;
|
||||
|
||||
public class ReceiveProofEvent {
|
||||
private final Wallet wallet;
|
||||
private final Aopp aopp;
|
||||
|
||||
public ReceiveProofEvent(Wallet wallet, Aopp aopp) {
|
||||
this.wallet = wallet;
|
||||
this.aopp = aopp;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public Aopp getAopp() {
|
||||
return aopp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
|
||||
public class SelectEntryEvent {
|
||||
private final Entry entry;
|
||||
|
||||
public SelectEntryEvent(Entry entry) {
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
public Entry getEntry() {
|
||||
return entry;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return entry.getWallet();
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,30 @@ package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.wallet.Function;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SendActionEvent {
|
||||
private final Wallet wallet;
|
||||
public class SendActionEvent extends FunctionActionEvent {
|
||||
private final List<BlockTransactionHashIndex> utxos;
|
||||
private final boolean selectIfEmpty;
|
||||
|
||||
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this(wallet, utxos, false);
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
public SendActionEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, boolean selectIfEmpty) {
|
||||
super(Function.SEND, wallet);
|
||||
this.utxos = utxos;
|
||||
this.selectIfEmpty = selectIfEmpty;
|
||||
}
|
||||
|
||||
public List<BlockTransactionHashIndex> getUtxos() {
|
||||
return utxos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean selectFunction() {
|
||||
return selectIfEmpty || !getUtxos().isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,6 @@ public class SettingsChangedEvent {
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE;
|
||||
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
public class ShowTransactionsCountEvent {
|
||||
private final boolean showCount;
|
||||
|
||||
public ShowTransactionsCountEvent(boolean showCount) {
|
||||
this.showCount = showCount;
|
||||
}
|
||||
|
||||
public boolean isShowCount() {
|
||||
return showCount;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -15,19 +16,21 @@ public class SpendUtxoEvent {
|
||||
private final Long fee;
|
||||
private final boolean includeSpentMempoolOutputs;
|
||||
private final Pool pool;
|
||||
private final PaymentCode paymentCode;
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
this(wallet, utxos, null, null, false);
|
||||
this(wallet, utxos, null, null, null, false);
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, boolean includeSpentMempoolOutputs) {
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, boolean includeSpentMempoolOutputs) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this.payments = payments;
|
||||
this.opReturns = null;
|
||||
this.opReturns = opReturns;
|
||||
this.fee = fee;
|
||||
this.includeSpentMempoolOutputs = includeSpentMempoolOutputs;
|
||||
this.pool = null;
|
||||
this.paymentCode = null;
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, Pool pool) {
|
||||
@@ -38,6 +41,18 @@ public class SpendUtxoEvent {
|
||||
this.fee = fee;
|
||||
this.includeSpentMempoolOutputs = false;
|
||||
this.pool = pool;
|
||||
this.paymentCode = null;
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<Payment> payments, List<byte[]> opReturns, PaymentCode paymentCode) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = null;
|
||||
this.payments = payments;
|
||||
this.opReturns = opReturns;
|
||||
this.fee = null;
|
||||
this.includeSpentMempoolOutputs = false;
|
||||
this.pool = null;
|
||||
this.paymentCode = paymentCode;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
@@ -67,4 +82,8 @@ public class SpendUtxoEvent {
|
||||
public Pool getPool() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
public PaymentCode getPaymentCode() {
|
||||
return paymentCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import javafx.scene.Node;
|
||||
|
||||
public class StatusEvent {
|
||||
public static final int DEFAULT_SHOW_DURATION_SECS = 20;
|
||||
|
||||
private final String status;
|
||||
private final Node graphic;
|
||||
private final int showDuration;
|
||||
|
||||
public StatusEvent(String status) {
|
||||
this(status, DEFAULT_SHOW_DURATION_SECS);
|
||||
this(status, null, DEFAULT_SHOW_DURATION_SECS);
|
||||
}
|
||||
|
||||
public StatusEvent(String status, Node graphic) {
|
||||
this(status, graphic, DEFAULT_SHOW_DURATION_SECS);
|
||||
}
|
||||
|
||||
public StatusEvent(String status, int showDuration) {
|
||||
this(status, null, showDuration);
|
||||
}
|
||||
|
||||
public StatusEvent(String status, Node graphic, int showDuration) {
|
||||
this.status = status;
|
||||
this.graphic = graphic;
|
||||
this.showDuration = showDuration;
|
||||
}
|
||||
|
||||
@@ -19,6 +31,10 @@ public class StatusEvent {
|
||||
return status;
|
||||
}
|
||||
|
||||
public Node getGraphic() {
|
||||
return graphic;
|
||||
}
|
||||
|
||||
public int getShowDuration() {
|
||||
return showDuration;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
|
||||
public class TransactionFetchFailedEvent extends TransactionReferencesFailedEvent {
|
||||
public TransactionFetchFailedEvent(Transaction transaction, Throwable exception) {
|
||||
super(transaction, exception);
|
||||
}
|
||||
|
||||
public TransactionFetchFailedEvent(Transaction transaction, Throwable exception, int pageStart, int pageEnd) {
|
||||
super(transaction, exception, pageStart, pageEnd);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user