mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 12:06:15 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
754ebf7bbf | ||
|
|
bc7a0be87e | ||
|
|
87af1ed9f5 | ||
|
|
da476c9d77 | ||
|
|
d8e4582b3c | ||
|
|
ad5c695946 | ||
|
|
3150a96aab | ||
|
|
28521cbc1a | ||
|
|
79bbe7df9f | ||
|
|
e339c9f51a | ||
|
|
6dd1bda0cb | ||
|
|
9fe0d17aac | ||
|
|
a2eb937fce | ||
|
|
e985a03a58 | ||
|
|
273d2aacf3 | ||
|
|
97383a4e35 | ||
|
|
712750a25c | ||
|
|
1be9ac1072 | ||
|
|
a035767e38 | ||
|
|
1ad237c623 | ||
|
|
7e4aaacd2f | ||
|
|
60d4cce15b | ||
|
|
7afb27b3e5 | ||
|
|
8f56e95048 | ||
|
|
1a5f97cecf | ||
|
|
db3ebdb300 | ||
|
|
0b012995a0 | ||
|
|
9b12361724 | ||
|
|
201d4b8376 | ||
|
|
ab6416f30a | ||
|
|
e64069f02f | ||
|
|
e96a7113b0 | ||
|
|
abe1f9c9e5 | ||
|
|
69aef9c228 | ||
|
|
630ce0f3ed | ||
|
|
723b004e5f | ||
|
|
6cefec5cec | ||
|
|
f5ee7bf277 | ||
|
|
0459f4ca45 | ||
|
|
c23dbeedcf | ||
|
|
0bb5e9319f | ||
|
|
1c5602aa9d | ||
|
|
c67008840d | ||
|
|
17a04510fd | ||
|
|
dd50f6973b | ||
|
|
8780e515d5 | ||
|
|
110f887952 | ||
|
|
1143eaa55f | ||
|
|
3068ba3988 | ||
|
|
58ca52e4c7 | ||
|
|
883b7cf3b2 | ||
|
|
ee5b502a00 | ||
|
|
4d7baa070e | ||
|
|
fb48643466 | ||
|
|
e2c38c2ac4 | ||
|
|
e55dd4401b | ||
|
|
53b6e2532f | ||
|
|
98ae891898 | ||
|
|
019b11c95e | ||
|
|
186eacc245 | ||
|
|
32a35ed2c0 | ||
|
|
21f9f9fe25 | ||
|
|
6c6664f29a | ||
|
|
ea0509ca3d | ||
|
|
67e1573329 | ||
|
|
23a9db5fc6 | ||
|
|
e9108b85cd | ||
|
|
3766585474 | ||
|
|
0e7aa34d07 | ||
|
|
537c2ffef0 | ||
|
|
8b4f388eb7 | ||
|
|
6a07adfea5 | ||
|
|
eeade6a173 | ||
|
|
4c2b8055ec | ||
|
|
04d210ed4b | ||
|
|
5b737be76d | ||
|
|
908fb80d2a | ||
|
|
eda7087bdd | ||
|
|
a5d61f1a9f | ||
|
|
2621bb6504 | ||
|
|
7dfc432bd2 | ||
|
|
df5b5146ba | ||
|
|
452584d87d | ||
|
|
5c823533d3 | ||
|
|
6561971d4f | ||
|
|
8ebf8dc041 | ||
|
|
419e8e1374 | ||
|
|
684965cf89 | ||
|
|
3498b5bab1 | ||
|
|
74635b1d1f | ||
|
|
229c6430ed | ||
|
|
c74abfeeb7 | ||
|
|
c78bc7b1cf | ||
|
|
87cd96a627 | ||
|
|
7ae04a2564 | ||
|
|
d4c0df0df9 | ||
|
|
dc88dc4ede | ||
|
|
a478edfad7 | ||
|
|
46d444615c | ||
|
|
78fe55787b | ||
|
|
cd0be365de | ||
|
|
b9d62c0b7b | ||
|
|
53d1f196e4 | ||
|
|
64b8da14a6 | ||
|
|
529cc3d6e7 | ||
|
|
e13fe897e1 | ||
|
|
dac1a61601 | ||
|
|
227c7ae637 | ||
|
|
e11675b9a8 | ||
|
|
146e6cbdc4 | ||
|
|
e4fa0c9356 | ||
|
|
30719c5a7f | ||
|
|
a120d08ea1 | ||
|
|
b51bd90261 | ||
|
|
a765105d34 | ||
|
|
2cd7d0bdf3 | ||
|
|
c6c24028d9 | ||
|
|
efb81555bd | ||
|
|
b11afdf2bb | ||
|
|
779a55c494 | ||
|
|
b06d858794 | ||
|
|
306c025cbf | ||
|
|
b145bddf07 | ||
|
|
edee72d97d | ||
|
|
b87650a740 | ||
|
|
0c9bbc2550 | ||
|
|
088f8e302c | ||
|
|
4309216ad7 | ||
|
|
dffaf806cd | ||
|
|
448067db54 | ||
|
|
6b498c28c2 | ||
|
|
4724dc7700 | ||
|
|
883558fd9c | ||
|
|
9d576bc45d | ||
|
|
49d807f31b | ||
|
|
0c2ee6690f | ||
|
|
85c81adc09 | ||
|
|
04de83706e | ||
|
|
ab99f1d392 | ||
|
|
53857389b7 | ||
|
|
98576d40c6 | ||
|
|
96f7d4bdb3 | ||
|
|
0c67962730 | ||
|
|
0515d8056b | ||
|
|
cf15760d34 | ||
|
|
f28f15df66 | ||
|
|
34900d2909 | ||
|
|
430f2ee320 | ||
|
|
74160ba35f | ||
|
|
59d85cdd05 | ||
|
|
0406421918 | ||
|
|
4ddd09fe22 | ||
|
|
a49edb7c45 | ||
|
|
5275cab436 | ||
|
|
4dcd463d92 | ||
|
|
f900f6dcb2 | ||
|
|
b0ab6c9d42 | ||
|
|
b8a703add2 | ||
|
|
4185a8dced | ||
|
|
a608197038 | ||
|
|
b4630043eb | ||
|
|
6452bf8184 | ||
|
|
71cb5852b3 | ||
|
|
8efee4eaae | ||
|
|
ac044c6f71 | ||
|
|
d2d45e5432 | ||
|
|
c16997eacc | ||
|
|
21543de007 | ||
|
|
6f772cb193 | ||
|
|
a4634f6a3d | ||
|
|
a794dafdf9 | ||
|
|
9fb19ebe8b | ||
|
|
46a39225bd | ||
|
|
a0f7e2e65d | ||
|
|
39cb946e08 | ||
|
|
0894407b0b | ||
|
|
c76acb8d82 | ||
|
|
16e73ebc32 | ||
|
|
3a5fa69fb6 | ||
|
|
4774830ce4 | ||
|
|
2f62a9e9c8 | ||
|
|
75bcfe2253 | ||
|
|
bedf1399ea | ||
|
|
58575793ea | ||
|
|
6c9b580d4f | ||
|
|
31909b7a15 | ||
|
|
092267339a | ||
|
|
0974918cff | ||
|
|
0f4c36b3c2 | ||
|
|
e1fe35fb74 | ||
|
|
d37fd00c4b | ||
|
|
5f54f86df7 | ||
|
|
e2fa3df08d | ||
|
|
6d6ede9abe | ||
|
|
cca9ab1056 | ||
|
|
9e33861110 | ||
|
|
c3d3fd1fda | ||
|
|
ca8553ecb8 | ||
|
|
d23ee8c086 | ||
|
|
e776a17ad4 | ||
|
|
480ce1e476 | ||
|
|
656cd90b08 | ||
|
|
8df0777959 | ||
|
|
84566b92e6 | ||
|
|
7802510e58 | ||
|
|
efb1eb1051 | ||
|
|
6240667478 | ||
|
|
2c27112dad | ||
|
|
6d53e1ed1d | ||
|
|
e8c5660897 | ||
|
|
bef6c750bd | ||
|
|
4ec3603789 | ||
|
|
90c9f9733f | ||
|
|
64efcf67d3 | ||
|
|
385d173948 | ||
|
|
d81b868049 | ||
|
|
2ff7a15d1e | ||
|
|
f48fa7e23c | ||
|
|
4632850e1e | ||
|
|
5f62523710 | ||
|
|
9dcf210762 | ||
|
|
c7e9a0a161 | ||
|
|
fa10714844 | ||
|
|
80105aee62 | ||
|
|
3c5fa58a16 | ||
|
|
2a2be2617c | ||
|
|
6c9a0d14cd | ||
|
|
f82fcb58bb | ||
|
|
5ec3bff6a4 | ||
|
|
134dc826ba | ||
|
|
cd2a6623a4 | ||
|
|
49ab9e40e3 | ||
|
|
cec7eac9ac | ||
|
|
33e043fd9a | ||
|
|
3aae26b196 | ||
|
|
73d4fd5049 | ||
|
|
a94380e882 | ||
|
|
e4dd4950bf | ||
|
|
26ce1b3469 | ||
|
|
ebce34f3d1 | ||
|
|
f28e00b97e | ||
|
|
25770c2426 | ||
|
|
799cac7b1f | ||
|
|
c265fd1969 | ||
|
|
890f0476b1 | ||
|
|
4d93381124 | ||
|
|
364909cfa3 | ||
|
|
38f0068411 | ||
|
|
8885e48ed9 | ||
|
|
31ce3ce68a | ||
|
|
b0d0514617 | ||
|
|
d7d23f9b58 | ||
|
|
3fdf093a26 | ||
|
|
74c298fd93 | ||
|
|
4298bfb053 | ||
|
|
231eb13cee | ||
|
|
52470ee6d8 | ||
|
|
853949675e | ||
|
|
098afebbe0 | ||
|
|
63c0a6d6e2 | ||
|
|
77c305f90b | ||
|
|
276f8b4148 | ||
|
|
b3c92617c9 | ||
|
|
58635801fc | ||
|
|
8c32bb3903 | ||
|
|
55a2c86a83 | ||
|
|
345e018eb9 | ||
|
|
45d2dee764 | ||
|
|
250bc84060 | ||
|
|
c3dba8ede6 | ||
|
|
db478f8da6 | ||
|
|
4ab9a9f681 | ||
|
|
c078aea3b4 | ||
|
|
af4a283b3f | ||
|
|
892885c0b1 | ||
|
|
d4a1441d65 | ||
|
|
1605cd2619 | ||
|
|
b4d34aacc5 | ||
|
|
1a4f0113c7 | ||
|
|
055e3ac496 | ||
|
|
d0da85171c | ||
|
|
af4c68a09c | ||
|
|
b1ab157ee3 | ||
|
|
94b27ba7e8 | ||
|
|
e697313259 | ||
|
|
1b0e5e9726 | ||
|
|
df0c4310ca | ||
|
|
474f3a4e91 | ||
|
|
c6e42d8fe2 | ||
|
|
3698ca8e85 | ||
|
|
53c5a8d2df | ||
|
|
3d85491e6b | ||
|
|
c77f52f7f6 | ||
|
|
e3138f3392 | ||
|
|
7a4015fdb5 | ||
|
|
94d15c09e6 | ||
|
|
71ac72e9f6 | ||
|
|
be8b56e355 | ||
|
|
af8505c0eb | ||
|
|
5edabf2e14 | ||
|
|
c73ebdc8a2 | ||
|
|
c9d7b8ef9a | ||
|
|
b3a6340c45 | ||
|
|
0975d12155 | ||
|
|
e31aa7fc80 | ||
|
|
b777c8c64d | ||
|
|
4176f76ffc | ||
|
|
64dac72f4f | ||
|
|
e29559f59c | ||
|
|
b1223ef064 | ||
|
|
6f0a30cc25 | ||
|
|
2fa8e5fd70 | ||
|
|
a8f7ce9e34 | ||
|
|
c946ef7479 | ||
|
|
7fa13901d4 | ||
|
|
8a88488a42 | ||
|
|
25a3f5539d | ||
|
|
520c5f2cfa | ||
|
|
d8877a259c | ||
|
|
7de63b2b5f | ||
|
|
f1c4b8aa69 | ||
|
|
6f6d61fb75 | ||
|
|
2c4de99fad | ||
|
|
3e197eb310 | ||
|
|
bd5af560ff | ||
|
|
3b9551a8c6 | ||
|
|
289a4453a4 | ||
|
|
27e21c890f | ||
|
|
4239a56bc1 | ||
|
|
5c9de07d48 | ||
|
|
9a8a25344a | ||
|
|
be86b4feaa | ||
|
|
37763e9557 | ||
|
|
80c4f4f5f6 | ||
|
|
6c3fe93d1e | ||
|
|
76eff2de48 | ||
|
|
07a6818823 | ||
|
|
2253a1bb97 | ||
|
|
36ee8add08 | ||
|
|
883e75c0df | ||
|
|
cc908b09c7 | ||
|
|
ce963ed5b6 | ||
|
|
951e33dc06 | ||
|
|
6a6a6b1cca | ||
|
|
8953d404fa | ||
|
|
b366177782 | ||
|
|
d0c827c2c7 | ||
|
|
5c29bf51b7 | ||
|
|
d426703dcc | ||
|
|
78f0721168 | ||
|
|
20d3f07059 | ||
|
|
1140a678ad | ||
|
|
6e8d44bc8c | ||
|
|
ad3b384feb | ||
|
|
f38350b38d | ||
|
|
62060c9839 | ||
|
|
8975f6f666 | ||
|
|
c7351cd191 | ||
|
|
62b1dc3900 | ||
|
|
f37ff47850 | ||
|
|
cfaa1f6c6e | ||
|
|
91c94b94eb | ||
|
|
a5eb7da067 | ||
|
|
195dbcef3b | ||
|
|
24955604e2 | ||
|
|
0305afbc02 | ||
|
|
d4c3c3afa8 | ||
|
|
cda7835672 | ||
|
|
b4b679dd16 | ||
|
|
3efaec2859 | ||
|
|
a53812c12f | ||
|
|
686c008e97 | ||
|
|
4d60a20336 | ||
|
|
9879889875 | ||
|
|
4aee89a35b | ||
|
|
fd9648efd1 | ||
|
|
8f438cd0bc | ||
|
|
8b47701dbe | ||
|
|
ff571c3df4 | ||
|
|
201c9ccca3 | ||
|
|
cbae280cc8 | ||
|
|
1f44229e62 | ||
|
|
541c71a002 | ||
|
|
bb08b5599c | ||
|
|
fd70f03259 | ||
|
|
fbca6c691d | ||
|
|
e8cd56388f | ||
|
|
3dfd8210a8 | ||
|
|
f9199b65f0 | ||
|
|
6a001bd67f | ||
|
|
ee2f387cd5 | ||
|
|
95200c7143 | ||
|
|
d7511c62bf | ||
|
|
7a5f4ff294 | ||
|
|
2b145cb9cc | ||
|
|
13bd05853c | ||
|
|
59f3338842 | ||
|
|
2cc38dc8b0 | ||
|
|
0e9d97c221 | ||
|
|
fb0fd013d9 | ||
|
|
e7510d2275 | ||
|
|
e92d0f9b58 | ||
|
|
ea23bb51d9 | ||
|
|
2d3bf0b2fe | ||
|
|
617ad380c0 | ||
|
|
29ac15846d | ||
|
|
f4acd3e587 | ||
|
|
f057b92729 | ||
|
|
4bf02f833c | ||
|
|
7ef51e6a5d | ||
|
|
fdbcea1625 | ||
|
|
218c2720e0 | ||
|
|
91ad82a21c | ||
|
|
f4b3b3d55a | ||
|
|
db1b55cfa0 | ||
|
|
bd0aca66b5 | ||
|
|
22ad1cc5d1 | ||
|
|
d07a5f0a01 | ||
|
|
947013e088 | ||
|
|
25f441a6a8 | ||
|
|
ee5015f0d5 | ||
|
|
4f00fabd23 | ||
|
|
6927423d68 | ||
|
|
fffa636956 | ||
|
|
a02ac3dcd2 | ||
|
|
e56e3d9394 | ||
|
|
119d00233d | ||
|
|
da427610d6 | ||
|
|
46034b8f11 | ||
|
|
d49d5967b2 | ||
|
|
484ef5f399 | ||
|
|
740c00d1ba | ||
|
|
dfae39255e | ||
|
|
c2bce893db | ||
|
|
ef063fde75 | ||
|
|
adb446de3e | ||
|
|
d040f186a2 | ||
|
|
b4f9c52413 | ||
|
|
7527dd0909 | ||
|
|
b0be8ca7c2 | ||
|
|
1e0c0c1c75 | ||
|
|
d731f7296b | ||
|
|
12034a07d7 | ||
|
|
60e3d4e107 | ||
|
|
ad8e17a3a0 | ||
|
|
3e676eadcb | ||
|
|
3640db3d3d | ||
|
|
d0bf55de70 | ||
|
|
ad0b6adfd8 | ||
|
|
92b32b0d99 | ||
|
|
233addc1b7 | ||
|
|
1d8c37066e | ||
|
|
c450efe499 | ||
|
|
34bcc87468 | ||
|
|
2aac365039 | ||
|
|
7e68ecffd3 | ||
|
|
bf457620db | ||
|
|
e50fe4c68c | ||
|
|
1bbc586cd6 | ||
|
|
e1dab3a48e | ||
|
|
73b672a7ef | ||
|
|
b142c54c68 | ||
|
|
58d09c3ba7 | ||
|
|
d5a7a5b855 | ||
|
|
fcb83f8bfa | ||
|
|
f187603ec3 | ||
|
|
8d7308bc37 | ||
|
|
e44d1393f5 | ||
|
|
33ba472843 | ||
|
|
faa81f2273 | ||
|
|
0646c8aa28 | ||
|
|
deb47ca002 | ||
|
|
ec131bb8da | ||
|
|
31f287125f | ||
|
|
e131f645f6 | ||
|
|
eabc0da6d5 | ||
|
|
49573d1075 | ||
|
|
17093dbf72 | ||
|
|
c2b5b24702 | ||
|
|
65f1fa7cf8 | ||
|
|
cbee341544 | ||
|
|
95b1aa8e48 | ||
|
|
af89be96e5 | ||
|
|
fad960c192 | ||
|
|
1adeef04db | ||
|
|
47f925b677 | ||
|
|
5db3096386 | ||
|
|
62e98b0090 | ||
|
|
76490604ac | ||
|
|
783733b9d3 | ||
|
|
041b5aa34c | ||
|
|
33d23e9ea5 | ||
|
|
b3f6cc88f0 | ||
|
|
b912aa2eb9 | ||
|
|
d894343457 | ||
|
|
fb1e1cefda | ||
|
|
d960bdce96 | ||
|
|
fb679c0199 | ||
|
|
9ecfe0e94f | ||
|
|
1bc2f7d69f | ||
|
|
6e4d308c79 | ||
|
|
afb6037449 | ||
|
|
369983748d |
@@ -10,47 +10,64 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-2022, ubuntu-20.04, macos-12]
|
||||
os: [windows-2022, ubuntu-22.04, ubuntu-22.04-arm, macos-15-intel, macos-14]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 18.0.1
|
||||
uses: actions/setup-java@v4
|
||||
submodules: recursive
|
||||
- name: Clear Java tool-cache for reproducibility
|
||||
shell: bash
|
||||
run: rm -rf "$RUNNER_TOOL_CACHE"/Java_*
|
||||
- name: Set up JDK 25.0.2
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '18.0.1'
|
||||
java-version: '25.0.2'
|
||||
- name: Show Build Versions
|
||||
run: ./gradlew -v
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew jpackage
|
||||
- name: Package zip distribution
|
||||
if: ${{ runner.os == 'Windows' || runner.os == 'macOS' }}
|
||||
- name: Codesign, package and notarize macOS distribution
|
||||
if: ${{ runner.os == 'macOS' }}
|
||||
uses: sparrowwallet/github-actions/codesign-macos@v1
|
||||
with:
|
||||
app-name: Sparrow
|
||||
certificate: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
certificate-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
|
||||
apple-id: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
|
||||
team-id: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
|
||||
notarization-password: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
|
||||
- name: Package Windows zip distribution
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
run: ./gradlew packageZipDistribution
|
||||
- name: Package tar distribution
|
||||
- name: Package Linux tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew packageTarDistribution
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Repackage Linux deb distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./repackage.sh
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }}
|
||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }}
|
||||
path: |
|
||||
build/jpackage/*
|
||||
!build/jpackage/Sparrow/
|
||||
!build/jpackage/Sparrow.app/
|
||||
- name: Headless build with Gradle
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew -Djava.awt.headless=true clean jpackage
|
||||
- name: Package headless tar distribution
|
||||
- name: Package Linux headless tar distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./gradlew -Djava.awt.headless=true packageTarDistribution
|
||||
- name: Rename Headless Artifacts
|
||||
- name: Repackage Linux headless deb distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: for f in build/jpackage/sparrow*; do mv -v "$f" "${f/sparrow/sparrow-server}"; done;
|
||||
run: ./repackage.sh
|
||||
- name: Upload Headless Artifact
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }} Headless
|
||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }} Headless
|
||||
path: |
|
||||
build/jpackage/*
|
||||
!build/jpackage/Sparrow/
|
||||
@@ -1,3 +1,6 @@
|
||||
[submodule "drongo"]
|
||||
path = drongo
|
||||
url = ../../sparrowwallet/drongo.git
|
||||
[submodule "lark"]
|
||||
path = lark
|
||||
url = ../../sparrowwallet/lark.git
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Enable auto-env through the sdkman_auto_env config
|
||||
# Add key=value pairs of SDKs to use below
|
||||
java=25.0.2-tem
|
||||
@@ -16,14 +16,14 @@ or for those without SSH credentials:
|
||||
|
||||
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
|
||||
|
||||
In order to build, Sparrow requires Java 18 or higher to be installed.
|
||||
The release binaries are built with [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
|
||||
In order to build, Sparrow requires Java 25 or higher to be installed.
|
||||
The release binaries are built with [Eclipse Temurin 25.0.2+10](https://github.com/adoptium/temurin25-binaries/releases/tag/jdk-25.0.2%2B10).
|
||||
If you are using [SDKMAN](https://sdkman.io/), you can use `sdk env install` to ensure you have the correct version.
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
|
||||
`sudo apt install -y rpm fakeroot binutils`
|
||||
|
||||
|
||||
The Sparrow binaries can be built from source using
|
||||
|
||||
`./gradlew jpackage`
|
||||
@@ -44,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
|
||||
|
||||
`./sparrow`
|
||||
|
||||
Java 18 or higher must be installed.
|
||||
Java 25 or higher must be installed.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
+282
-379
@@ -1,59 +1,38 @@
|
||||
import java.awt.GraphicsEnvironment
|
||||
|
||||
plugins {
|
||||
id 'application'
|
||||
id 'extra-java-module-info'
|
||||
id 'org-openjfx-javafxplugin'
|
||||
id 'org.beryx.jlink' version '3.0.1'
|
||||
id 'org.openjfx.javafxplugin' version '0.1.0'
|
||||
id 'org.beryx.jlink' version '3.2.1'
|
||||
id 'org.gradlex.extra-java-module-info' version '1.13.1'
|
||||
id 'io.matthewnelson.kmp.tor.resource-filterjar' version '408.21.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.9.1'
|
||||
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"))
|
||||
|
||||
def vTor = '4.7.13-4'
|
||||
def vKmpTor = '1.4.3'
|
||||
def kmpOs = osName
|
||||
if(os.macOsX) {
|
||||
kmpOs = "macos"
|
||||
} else if(os.windows) {
|
||||
kmpOs = "mingw"
|
||||
}
|
||||
def kmpArch = "x64"
|
||||
if(System.getProperty("os.arch") == "aarch64") {
|
||||
kmpArch = "arm64"
|
||||
}
|
||||
|
||||
group "com.sparrowwallet"
|
||||
version "${sparrowVersion}"
|
||||
group = 'com.sparrowwallet'
|
||||
version = '2.5.1'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://oss.sonatype.org/content/groups/public' }
|
||||
maven { url 'https://mymavenrepo.com/repo/29EACwkkGcoOKnbx3bxN/' }
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url 'https://maven.ecs.soton.ac.uk/content/groups/maven.openimaj.org/' }
|
||||
maven { url = uri('https://code.sparrowwallet.com/api/packages/sparrowwallet/maven') }
|
||||
}
|
||||
|
||||
tasks.withType(AbstractArchiveTask) {
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
tasks.withType(AbstractArchiveTask).configureEach {
|
||||
useFileSystemPermissions()
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = "18"
|
||||
version = "26"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
@@ -64,25 +43,26 @@ java {
|
||||
dependencies {
|
||||
//Any changes to the dependencies must be reflected in the module definitions below!
|
||||
implementation(project(':drongo'))
|
||||
implementation('com.google.guava:guava:33.0.0-jre')
|
||||
implementation('com.google.code.gson:gson:2.9.1')
|
||||
implementation(project(':lark'))
|
||||
implementation('com.google.guava:guava:33.5.0-jre')
|
||||
implementation('com.google.code.gson:gson:2.13.2')
|
||||
implementation('com.h2database:h2:2.1.214')
|
||||
implementation('com.zaxxer:HikariCP:4.0.3') {
|
||||
implementation('com.zaxxer:HikariCP:7.0.2') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-core:3.20.0') {
|
||||
implementation('org.jdbi:jdbi3-core:3.51.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.20.0') {
|
||||
implementation('org.jdbi:jdbi3-sqlobject:3.51.0') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('org.flywaydb:flyway-core:7.10.7-SNAPSHOT')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.10.4')
|
||||
implementation('org.flywaydb:flyway-core:9.22.3')
|
||||
implementation('org.fxmisc.richtext:richtextfx:0.11.7')
|
||||
implementation('no.tornado:tornadofx-controls:1.0.4')
|
||||
implementation('com.google.zxing:javase:3.4.0') {
|
||||
implementation('com.google.zxing:javase:3.5.4') {
|
||||
exclude group: 'com.beust', module: 'jcommander'
|
||||
}
|
||||
implementation('com.beust:jcommander:1.81')
|
||||
implementation('org.jcommander:jcommander:3.0')
|
||||
implementation('com.github.arteam:simple-json-rpc-core:1.3')
|
||||
implementation('com.github.arteam:simple-json-rpc-client:1.3') {
|
||||
exclude group: 'com.github.arteam', module: 'simple-json-rpc-core'
|
||||
@@ -90,25 +70,19 @@ dependencies {
|
||||
implementation('com.github.arteam:simple-json-rpc-server:1.3') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet:hummingbird:1.7.3')
|
||||
implementation('com.fasterxml.jackson.core:jackson-databind:2.21.1')
|
||||
implementation('com.sparrowwallet:hummingbird:1.7.4')
|
||||
implementation('co.nstant.in:cbor:0.9')
|
||||
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
|
||||
exclude group: 'com.google.android.tools', module: 'dx'
|
||||
implementation('org.openpnp:openpnp-capture-java:0.0.30-1')
|
||||
implementation("io.matthewnelson.kmp-tor:runtime:2.5.0")
|
||||
implementation("io.matthewnelson.kmp-tor:resource-exec-tor-gpl:408.21.0")
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.10.2') {
|
||||
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-common'
|
||||
}
|
||||
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
|
||||
exclude group: 'com.nativelibs4java', module: 'bridj'
|
||||
implementation('de.jangassen:nsmenufx:3.1.0') {
|
||||
exclude group: 'net.java.dev.jna', module: 'jna'
|
||||
}
|
||||
implementation "io.matthewnelson.kotlin-components:kmp-tor:${vTor}-${vKmpTor}"
|
||||
if(kmpOs == "linux" && kmpArch == "arm64") {
|
||||
implementation("com.sparrowwallet.kmp-tor-binary:kmp-tor-binary-${kmpOs}${kmpArch}-jvm:${vTor}")
|
||||
} else {
|
||||
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-${kmpOs}${kmpArch}:${vTor}")
|
||||
}
|
||||
implementation("io.matthewnelson.kotlin-components:kmp-tor-binary-extract:${vTor}")
|
||||
implementation("io.matthewnelson.kotlin-components:kmp-tor-ext-callback-manager:${vKmpTor}")
|
||||
implementation('org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.7.1')
|
||||
implementation('de.codecentric.centerdevice:centerdevice-nsmenufx:2.1.7')
|
||||
implementation('org.controlsfx:controlsfx:11.1.0' ) {
|
||||
implementation('org.controlsfx:controlsfx:11.2.3' ) {
|
||||
exclude group: 'org.openjfx', module: 'javafx-base'
|
||||
exclude group: 'org.openjfx', module: 'javafx-graphics'
|
||||
exclude group: 'org.openjfx', module: 'javafx-controls'
|
||||
@@ -119,25 +93,25 @@ dependencies {
|
||||
}
|
||||
implementation('dev.bwt:bwt-jni:0.1.8')
|
||||
implementation('net.sourceforge.javacsv:javacsv:2.0')
|
||||
implementation ('org.slf4j:slf4j-api:2.0.12')
|
||||
implementation('org.slf4j:jul-to-slf4j:2.0.12') {
|
||||
implementation ('org.slf4j:slf4j-api:2.0.17')
|
||||
implementation('org.slf4j:jul-to-slf4j:2.0.17') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
||||
implementation('org.eclipse.jetty:jetty-client:9.4.54.v20240208')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('com.sparrowwallet:tern:1.0.6')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.21')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
implementation('org.apache.commons:commons-compress:1.25.0')
|
||||
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.30')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.1')
|
||||
implementation('net.coobird:thumbnailator:0.4.18')
|
||||
implementation('com.github.hervegirod:fxsvgimage:1.0b2')
|
||||
implementation('org.apache.commons:commons-lang3:3.20.0')
|
||||
implementation('org.apache.commons:commons-compress:1.28.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.43')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.3')
|
||||
implementation('net.coobird:thumbnailator:0.4.21')
|
||||
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||
implementation('com.jcraft:jzlib:1.1.3')
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.10.0')
|
||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.10.0')
|
||||
implementation('io.github.doblon8:jzbar:0.4.0')
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.14.1')
|
||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.14.1')
|
||||
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
||||
}
|
||||
|
||||
@@ -150,17 +124,9 @@ compileJava {
|
||||
}
|
||||
}
|
||||
|
||||
processResources {
|
||||
doLast {
|
||||
delete fileTree("$buildDir/resources/main/native").matching {
|
||||
exclude "${osName}/${osArch}/**"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--add-opens=java.base/java.io=com.google.gson"]
|
||||
jvmArgs = ["--add-opens=java.base/java.io=ALL-UNNAMED", "--enable-native-access=ALL-UNNAMED"]
|
||||
}
|
||||
|
||||
application {
|
||||
@@ -168,6 +134,12 @@ application {
|
||||
mainClass = 'com.sparrowwallet.sparrow.SparrowWallet'
|
||||
|
||||
applicationDefaultJvmArgs = ["-XX:+HeapDumpOnOutOfMemoryError",
|
||||
"--enable-native-access=com.sparrowwallet.drongo",
|
||||
"--enable-native-access=com.sun.jna",
|
||||
"--enable-native-access=javafx.graphics",
|
||||
"--enable-native-access=com.fazecast.jSerialComm",
|
||||
"--enable-native-access=org.usb4java",
|
||||
"--enable-native-access=io.github.doblon8.jzbar",
|
||||
"--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",
|
||||
@@ -177,25 +149,20 @@ application {
|
||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.net=com.sparrowwallet.sparrow",
|
||||
"--add-opens=java.base/java.io=com.google.gson",
|
||||
"--add-opens=java.smartcardio/sun.security.smartcardio=com.sparrowwallet.sparrow",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
|
||||
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||
|
||||
if(os.macOsX) {
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
|
||||
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow"]
|
||||
}
|
||||
if(headless) {
|
||||
applicationDefaultJvmArgs += ["-Dglass.platform=Monocle", "-Dmonocle.platform=Headless", "-Dprism.order=sw"]
|
||||
applicationDefaultJvmArgs += ["-Dglass.platform=Headless"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,15 +176,49 @@ jlink {
|
||||
requires 'jdk.crypto.cryptoki'
|
||||
requires 'java.management'
|
||||
requires 'io.leangen.geantyref'
|
||||
uses 'org.flywaydb.core.extensibility.FlywayExtension'
|
||||
uses 'org.flywaydb.core.internal.database.DatabaseType'
|
||||
requires 'static jdk.jfr'
|
||||
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
|
||||
}
|
||||
|
||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6',
|
||||
'--no-header-files', '--no-man-pages', '--ignore-signing-information',
|
||||
'--exclude-files', '**.png',
|
||||
'--exclude-resources',
|
||||
'glob:/com.sparrowwallet.merged.module/META-INF/*,' +
|
||||
'glob:/javafx.graphics/*.dylib,' +
|
||||
'glob:/javafx.graphics/*.so,' +
|
||||
'glob:/javafx.graphics/*.dll,' +
|
||||
'glob:/com.sparrowwallet.drongo/native/**,' +
|
||||
'glob:/com.sparrowwallet.sparrow/native/**,' +
|
||||
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.so,' +
|
||||
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.dylib,' +
|
||||
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.jnilib,' +
|
||||
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.dll,' +
|
||||
'glob:/com.sparrowwallet.merged.module/com/sun/jna/**/*.a,' +
|
||||
'glob:/com.sparrowwallet.merged.module/darwin-*/**,' +
|
||||
'glob:/com.sparrowwallet.merged.module/linux-*/**,' +
|
||||
'glob:/com.sparrowwallet.merged.module/win32-*/**,' +
|
||||
'glob:/org.usb4java/org/usb4java/darwin-*/**,' +
|
||||
'glob:/org.usb4java/org/usb4java/linux-*/**,' +
|
||||
'glob:/org.usb4java/org/usb4java/win32-*/**,' +
|
||||
'glob:/org.hid4java/darwin-*/**,' +
|
||||
'glob:/org.hid4java/linux-*/**,' +
|
||||
'glob:/org.hid4java/win32-*/**,' +
|
||||
'glob:/openpnp.capture.java/darwin-*/**,' +
|
||||
'glob:/openpnp.capture.java/linux-*/**,' +
|
||||
'glob:/openpnp.capture.java/win32-*/**,' +
|
||||
'glob:/io.github.doblon8.jzbar/native/**']
|
||||
launcher {
|
||||
name = 'sparrow'
|
||||
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
jvmArgs = ["--enable-native-access=com.sparrowwallet.drongo",
|
||||
"--enable-native-access=com.sun.jna",
|
||||
"--enable-native-access=javafx.graphics",
|
||||
"--enable-native-access=com.sparrowwallet.merged.module",
|
||||
"--enable-native-access=com.fazecast.jSerialComm",
|
||||
"--enable-native-access=org.usb4java",
|
||||
"--enable-native-access=io.github.doblon8.jzbar",
|
||||
"--enable-native-access=com.sparrowwallet.sparrow",
|
||||
"--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",
|
||||
@@ -226,11 +227,6 @@ jlink {
|
||||
"--add-opens=javafx.controls/javafx.scene.control.cell=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=com.sparrowwallet.sparrow",
|
||||
"--add-opens=org.controlsfx.controls/impl.org.controlsfx.skin=javafx.fxml",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.tk.quantum=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.controls/com.sun.javafx.scene.control=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.menu=centerdevice.nsmenufx",
|
||||
"--add-opens=javafx.graphics/com.sun.glass.ui=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/javafx.scene.input=com.sparrowwallet.sparrow",
|
||||
"--add-opens=javafx.graphics/com.sun.javafx.application=com.sparrowwallet.sparrow",
|
||||
@@ -248,7 +244,10 @@ jlink {
|
||||
"--add-reads=com.sparrowwallet.merged.module=co.nstant.in.cbor",
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.pg",
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.bouncycastle.provider",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core"]
|
||||
"--add-reads=com.sparrowwallet.merged.module=kotlin.stdlib",
|
||||
"--add-reads=com.sparrowwallet.merged.module=org.reactfx.reactfx",
|
||||
"--add-reads=kotlin.stdlib=kotlinx.coroutines.core",
|
||||
"--add-reads=org.flywaydb.core=java.desktop"]
|
||||
|
||||
if(os.windows) {
|
||||
jvmArgs += ["-Djavax.accessibility.assistive_technologies", "-Djavax.accessibility.screen_magnifier_present=false"]
|
||||
@@ -257,65 +256,125 @@ jlink {
|
||||
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"]
|
||||
jvmArgs += ["-Dglass.platform=Headless"]
|
||||
}
|
||||
}
|
||||
addExtraDependencies("javafx")
|
||||
jpackage {
|
||||
imageName = "Sparrow"
|
||||
installerName = "Sparrow"
|
||||
appVersion = "${sparrowVersion}"
|
||||
appVersion = "${version}"
|
||||
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/asc.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']
|
||||
installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/txn.properties', '--file-associations', 'src/main/deploy/asc.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']
|
||||
installerType = "exe"
|
||||
installerType = "msi"
|
||||
}
|
||||
if(os.linux) {
|
||||
if(headless) {
|
||||
installerName = "sparrowserver"
|
||||
installerOptions = ['--license-file', 'LICENSE']
|
||||
} else {
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||
installerName = "sparrowwallet"
|
||||
installerOptions += ['--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||
}
|
||||
installerOptions += ['--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
||||
installerOptions += ['--resource-dir', layout.buildDirectory.dir('deploy/package').get().asFile.toString(), '--linux-app-category', 'utils', '--linux-app-release', '1', '--linux-rpm-license-type', 'ASL 2.0', '--linux-deb-maintainer', 'mail@sparrowwallet.com']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/linux/Sparrow.png', '--resource-dir', 'src/main/deploy/package/linux/']
|
||||
}
|
||||
if(os.macOsX) {
|
||||
installerOptions += ['--mac-sign', '--mac-signing-key-user-name', 'Craig Raw (UPLVMSK9D7)']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/osx/sparrow.icns', '--resource-dir', 'src/main/deploy/package/osx/']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/macos/sparrow.icns', '--resource-dir', 'src/main/deploy/package/macos/']
|
||||
installerType = "dmg"
|
||||
}
|
||||
}
|
||||
if(os.linux) {
|
||||
jpackageImage {
|
||||
dependsOn('prepareModulesDir', 'copyUdevRules')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.jlink.finalizedBy "addUserWritePermission"
|
||||
if(os.linux) {
|
||||
tasks.jlink.finalizedBy('addUserWritePermission', 'copyUdevRules', 'extractNativeLibraries')
|
||||
tasks.jpackageImage.finalizedBy('prepareResourceDir')
|
||||
if(!headless) {
|
||||
tasks.jpackage.dependsOn('copyMimeInfo')
|
||||
}
|
||||
} else {
|
||||
tasks.jlink.finalizedBy('addUserWritePermission', 'extractNativeLibraries')
|
||||
}
|
||||
|
||||
task addUserWritePermission(type: Exec) {
|
||||
tasks.register('addUserWritePermission', Exec) {
|
||||
if(os.windows) {
|
||||
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T'
|
||||
def usersGroup = '*S-1-5-32-545' // Windows "Users" group SID (language-independent)
|
||||
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', "${usersGroup}:(OI)(CI)F", '/T'
|
||||
} else {
|
||||
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
||||
}
|
||||
}
|
||||
|
||||
task removeGroupWritePermission(type: Exec) {
|
||||
tasks.register('copyUdevRules', Copy) {
|
||||
from('lark/src/main/resources/udev')
|
||||
into(layout.buildDirectory.dir('image/conf/udev'))
|
||||
include('*')
|
||||
}
|
||||
|
||||
tasks.register('prepareResourceDir', Copy) {
|
||||
from("src/main/deploy/package/linux${headless ? '-headless' : ''}")
|
||||
into(layout.buildDirectory.dir('deploy/package'))
|
||||
include('*')
|
||||
eachFile { file ->
|
||||
if(file.name.equals('control') || file.name.endsWith('.spec')) {
|
||||
filter { line ->
|
||||
if(line.contains('${size}')) {
|
||||
line = line.replace('${size}', getDirectorySize(layout.buildDirectory.dir('jpackage/Sparrow').get().asFile))
|
||||
}
|
||||
return line.replace('${version}', "${version}").replace('${arch}', osArch == 'aarch64' ? 'arm64' : 'amd64')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('copyMimeInfo', Copy) {
|
||||
mustRunAfter tasks.jpackageImage
|
||||
from('src/main/deploy/package/linux')
|
||||
into(layout.buildDirectory.dir('jpackage/Sparrow/lib'))
|
||||
include('sparrowwallet-Sparrow-MimeInfo.xml')
|
||||
}
|
||||
|
||||
static def getDirectorySize(File directory) {
|
||||
long size = 0
|
||||
if(directory.isFile()) {
|
||||
size = directory.length()
|
||||
} else if(directory.isDirectory()) {
|
||||
directory.eachFileRecurse { file ->
|
||||
if(file.isFile()) {
|
||||
size += file.length()
|
||||
}
|
||||
}
|
||||
}
|
||||
return Long.toString(size/1024 as long)
|
||||
}
|
||||
|
||||
tasks.register('removeGroupWritePermission', Exec) {
|
||||
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
||||
}
|
||||
|
||||
task packageZipDistribution(type: Zip) {
|
||||
archiveFileName = "Sparrow-${sparrowVersion}.zip"
|
||||
tasks.register('packageZipDistribution', Zip) {
|
||||
archiveFileName = "Sparrow-${version}.zip"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
preserveFileTimestamps = os.macOsX
|
||||
from("$buildDir/jpackage/") {
|
||||
include "Sparrow/**"
|
||||
include "Sparrow.app/**"
|
||||
}
|
||||
}
|
||||
|
||||
task packageTarDistribution(type: Tar) {
|
||||
tasks.register('packageTarDistribution', Tar) {
|
||||
dependsOn removeGroupWritePermission
|
||||
archiveFileName = "sparrow-${sparrowVersion}-${releaseArch}.tar.gz"
|
||||
archiveFileName = "sparrow${headless ? 'server': 'wallet'}-${version}-${releaseArch}.tar.gz"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
compression = Compression.GZIP
|
||||
from("$buildDir/jpackage/") {
|
||||
@@ -323,61 +382,80 @@ task packageTarDistribution(type: Tar) {
|
||||
}
|
||||
}
|
||||
|
||||
def jnaPlatform
|
||||
if(os.macOsX) {
|
||||
jnaPlatform = "darwin-${osArch == 'aarch64' ? 'aarch64' : 'x86-64'}"
|
||||
} else if(os.windows) {
|
||||
jnaPlatform = "win32-x86-64"
|
||||
} else {
|
||||
jnaPlatform = "linux-${osArch == 'aarch64' ? 'aarch64' : 'x86-64'}"
|
||||
}
|
||||
|
||||
def serialOs = os.macOsX ? "OSX" : (os.windows ? "Windows" : "Linux")
|
||||
def serialArch = osArch == "aarch64" ? "aarch64" : "x86_64"
|
||||
|
||||
// Map of JAR name prefix to the include glob for platform-specific natives inside the JAR.
|
||||
def nativeLibJars = [
|
||||
'jna-' : "com/sun/jna/${jnaPlatform}/*",
|
||||
'argon2-jvm-2' : "${jnaPlatform}/*",
|
||||
'hid4java-' : "${jnaPlatform}/*",
|
||||
'openpnp-capture-java': "${jnaPlatform}/*",
|
||||
'jSerialComm-' : "${serialOs}/${serialArch}/*",
|
||||
'usb4java-' : "org/usb4java/${jnaPlatform}/*",
|
||||
'jzbar-' : "native/${osName}/${osArch}/*",
|
||||
]
|
||||
|
||||
tasks.register('extractNativeLibraries') {
|
||||
dependsOn 'jlink'
|
||||
doLast {
|
||||
def imageLib = file("$buildDir/image/lib")
|
||||
|
||||
// Project-owned natives
|
||||
copy {
|
||||
from "${project(':drongo').projectDir}/src/main/resources/native/${osName}/${osArch}", "src/main/resources/native/${osName}/${osArch}"
|
||||
into imageLib
|
||||
eachFile { it.permissions { unix('rw-r--r--') } }
|
||||
}
|
||||
|
||||
// JavaFX natives
|
||||
def javafxClassifier = ""
|
||||
if(os.macOsX) {
|
||||
javafxClassifier = osArch == "aarch64" ? "mac-aarch64" : "mac"
|
||||
} else if(os.windows) {
|
||||
javafxClassifier = "win"
|
||||
} else {
|
||||
javafxClassifier = osArch == "aarch64" ? "linux-aarch64" : "linux"
|
||||
}
|
||||
def javafxJar = configurations.runtimeClasspath.find { it.name == "javafx-graphics-${javafx.version}-${javafxClassifier}.jar" }
|
||||
if(javafxJar) {
|
||||
copy {
|
||||
from(zipTree(javafxJar)) { include "*.dylib", "*.so", "*.dll" }
|
||||
into imageLib
|
||||
eachFile { it.permissions { unix('rw-r--r--') } }
|
||||
}
|
||||
}
|
||||
|
||||
// Third-party natives
|
||||
nativeLibJars.each { prefix, includePattern ->
|
||||
def jar = configurations.runtimeClasspath.find { it.name.startsWith(prefix) }
|
||||
if(jar) {
|
||||
copy {
|
||||
from(zipTree(jar)) { include includePattern }
|
||||
into imageLib
|
||||
eachFile { it.path = it.name; it.permissions { unix('rw-r--r--') } }
|
||||
includeEmptyDirs = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extraJavaModuleInfo {
|
||||
module('jackson-core-2.13.2.jar', 'com.fasterxml.jackson.core', '2.13.2') {
|
||||
exports('com.fasterxml.jackson.core')
|
||||
exports('com.fasterxml.jackson.core.async')
|
||||
exports('com.fasterxml.jackson.core.base')
|
||||
exports('com.fasterxml.jackson.core.exc')
|
||||
exports('com.fasterxml.jackson.core.filter')
|
||||
exports('com.fasterxml.jackson.core.format')
|
||||
exports('com.fasterxml.jackson.core.io')
|
||||
exports('com.fasterxml.jackson.core.json')
|
||||
exports('com.fasterxml.jackson.core.json.async')
|
||||
exports('com.fasterxml.jackson.core.sym')
|
||||
exports('com.fasterxml.jackson.core.type')
|
||||
exports('com.fasterxml.jackson.core.util')
|
||||
uses('com.fasterxml.jackson.core.ObjectCodec')
|
||||
}
|
||||
module('jackson-annotations-2.13.2.jar', 'com.fasterxml.jackson.annotation', '2.13.2') {
|
||||
requires('com.fasterxml.jackson.core')
|
||||
exports('com.fasterxml.jackson.annotation')
|
||||
}
|
||||
module('jackson-databind-2.13.2.jar', 'com.fasterxml.jackson.databind', '2.13.2') {
|
||||
requires('java.desktop')
|
||||
requires('java.logging')
|
||||
requires('com.fasterxml.jackson.annotation')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
requires('java.sql')
|
||||
requires('java.xml')
|
||||
exports('com.fasterxml.jackson.databind')
|
||||
exports('com.fasterxml.jackson.databind.annotation')
|
||||
exports('com.fasterxml.jackson.databind.cfg')
|
||||
exports('com.fasterxml.jackson.databind.deser')
|
||||
exports('com.fasterxml.jackson.databind.deser.impl')
|
||||
exports('com.fasterxml.jackson.databind.deser.std')
|
||||
exports('com.fasterxml.jackson.databind.exc')
|
||||
exports('com.fasterxml.jackson.databind.ext')
|
||||
exports('com.fasterxml.jackson.databind.introspect')
|
||||
exports('com.fasterxml.jackson.databind.json')
|
||||
exports('com.fasterxml.jackson.databind.jsonFormatVisitors')
|
||||
exports('com.fasterxml.jackson.databind.jsonschema')
|
||||
exports('com.fasterxml.jackson.databind.jsontype')
|
||||
exports('com.fasterxml.jackson.databind.jsontype.impl')
|
||||
exports('com.fasterxml.jackson.databind.module')
|
||||
exports('com.fasterxml.jackson.databind.node')
|
||||
exports('com.fasterxml.jackson.databind.ser')
|
||||
exports('com.fasterxml.jackson.databind.ser.impl')
|
||||
exports('com.fasterxml.jackson.databind.ser.std')
|
||||
exports('com.fasterxml.jackson.databind.type')
|
||||
exports('com.fasterxml.jackson.databind.util')
|
||||
uses('com.fasterxml.jackson.databind.Module')
|
||||
}
|
||||
module('tornadofx-controls-1.0.4.jar', 'tornadofx.controls', '1.0.4') {
|
||||
module('no.tornado:tornadofx-controls', 'tornadofx.controls') {
|
||||
exports('tornadofx.control')
|
||||
requires('javafx.controls')
|
||||
}
|
||||
module('simple-json-rpc-core-1.3.jar', 'simple.json.rpc.core', '1.3') {
|
||||
module('com.github.arteam:simple-json-rpc-core', 'simple.json.rpc.core') {
|
||||
exports('com.github.arteam.simplejsonrpc.core.annotation')
|
||||
exports('com.github.arteam.simplejsonrpc.core.domain')
|
||||
requires('com.fasterxml.jackson.core')
|
||||
@@ -385,7 +463,7 @@ extraJavaModuleInfo {
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('org.jetbrains.annotations')
|
||||
}
|
||||
module('simple-json-rpc-client-1.3.jar', 'simple.json.rpc.client', '1.3') {
|
||||
module('com.github.arteam:simple-json-rpc-client', 'simple.json.rpc.client') {
|
||||
exports('com.github.arteam.simplejsonrpc.client')
|
||||
exports('com.github.arteam.simplejsonrpc.client.builder')
|
||||
exports('com.github.arteam.simplejsonrpc.client.exception')
|
||||
@@ -393,61 +471,26 @@ extraJavaModuleInfo {
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
requires('simple.json.rpc.core')
|
||||
}
|
||||
module('simple-json-rpc-server-1.3.jar', 'simple.json.rpc.server', '1.3') {
|
||||
module('com.github.arteam:simple-json-rpc-server', 'simple.json.rpc.server') {
|
||||
exports('com.github.arteam.simplejsonrpc.server')
|
||||
requires('simple.json.rpc.core')
|
||||
requires('com.google.common')
|
||||
requires('org.slf4j')
|
||||
requires('com.fasterxml.jackson.databind')
|
||||
}
|
||||
module("bridj${targetName}-0.7-20140918-3.jar", 'com.nativelibs4java.bridj', '0.7-20140918-3') {
|
||||
exports('org.bridj')
|
||||
exports('org.bridj.cpp')
|
||||
requires('java.logging')
|
||||
}
|
||||
module("webcam-capture${targetName}-0.3.13-SNAPSHOT.jar", 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
|
||||
exports('com.github.sarxos.webcam')
|
||||
exports('com.github.sarxos.webcam.ds.buildin')
|
||||
exports('com.github.sarxos.webcam.ds.buildin.natives')
|
||||
module('org.openpnp:openpnp-capture-java', 'openpnp.capture.java') {
|
||||
exports('org.openpnp.capture')
|
||||
exports('org.openpnp.capture.library')
|
||||
requires('java.desktop')
|
||||
requires('com.nativelibs4java.bridj')
|
||||
requires('org.slf4j')
|
||||
requires('com.sun.jna')
|
||||
}
|
||||
module('centerdevice-nsmenufx-2.1.7.jar', 'centerdevice.nsmenufx', '2.1.7') {
|
||||
exports('de.codecentric.centerdevice')
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('javacsv-2.0.jar', 'net.sourceforge.javacsv', '2.0') {
|
||||
module('net.sourceforge.javacsv:javacsv', 'net.sourceforge.javacsv') {
|
||||
exports('com.csvreader')
|
||||
}
|
||||
module('jeromq-0.5.0.jar', 'jeromq', '0.5.0') {
|
||||
exports('org.zeromq')
|
||||
}
|
||||
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
|
||||
exports('org.json.simple')
|
||||
exports('org.json.simple.parser')
|
||||
}
|
||||
module('listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar', 'com.google.guava.listenablefuture', '9999.0-empty-to-avoid-conflict-with-guava')
|
||||
module('jsr305-3.0.2.jar', 'com.google.code.findbugs.jsr305', '3.0.2')
|
||||
module('com.google.guava:listenablefuture|empty-to-avoid-conflict-with-guava', 'com.google.guava.listenablefuture')
|
||||
module('com.google.code.findbugs:jsr305', 'com.google.code.findbugs.jsr305')
|
||||
module('j2objc-annotations-2.8.jar', 'com.google.j2objc.j2objc.annotations', '2.8')
|
||||
module('jdbi3-core-3.20.0.jar', 'org.jdbi.v3.core', '3.20.0') {
|
||||
exports('org.jdbi.v3.core')
|
||||
exports('org.jdbi.v3.core.mapper')
|
||||
exports('org.jdbi.v3.core.statement')
|
||||
exports('org.jdbi.v3.core.result')
|
||||
exports('org.jdbi.v3.core.h2')
|
||||
exports('org.jdbi.v3.core.spi')
|
||||
requires('io.leangen.geantyref')
|
||||
requires('java.sql')
|
||||
requires('org.slf4j')
|
||||
requires('com.github.benmanes.caffeine')
|
||||
}
|
||||
module('geantyref-1.3.11.jar', 'io.leangen.geantyref', '1.3.11') {
|
||||
exports('io.leangen.geantyref')
|
||||
}
|
||||
module('richtextfx-0.10.4.jar', 'org.fxmisc.richtext', '0.10.4') {
|
||||
module('org.fxmisc.richtext:richtextfx', 'org.fxmisc.richtext') {
|
||||
exports('org.fxmisc.richtext')
|
||||
exports('org.fxmisc.richtext.event')
|
||||
exports('org.fxmisc.richtext.model')
|
||||
@@ -456,23 +499,23 @@ extraJavaModuleInfo {
|
||||
requires('javafx.graphics')
|
||||
requires('org.fxmisc.flowless')
|
||||
requires('org.reactfx.reactfx')
|
||||
requires('org.fxmisc.undo.undofx')
|
||||
requires('org.fxmisc.undo')
|
||||
requires('org.fxmisc.wellbehaved')
|
||||
}
|
||||
module('undofx-2.1.0.jar', 'org.fxmisc.undo.undofx', '2.1.0') {
|
||||
module('org.fxmisc.undo:undofx', 'org.fxmisc.undo') {
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
requires('org.reactfx.reactfx')
|
||||
}
|
||||
module('flowless-0.6.1.jar', 'org.fxmisc.flowless', '0.6.1') {
|
||||
module('org.fxmisc.flowless:flowless', 'org.fxmisc.flowless') {
|
||||
exports('org.fxmisc.flowless')
|
||||
requires('javafx.base')
|
||||
requires('javafx.controls')
|
||||
requires('javafx.graphics')
|
||||
requires('org.reactfx.reactfx')
|
||||
}
|
||||
module('reactfx-2.0-M5.jar', 'org.reactfx.reactfx', '2.0-M5') {
|
||||
module('org.reactfx:reactfx', 'org.reactfx.reactfx') {
|
||||
exports('org.reactfx')
|
||||
exports('org.reactfx.value')
|
||||
exports('org.reactfx.collection')
|
||||
@@ -481,187 +524,47 @@ extraJavaModuleInfo {
|
||||
requires('javafx.graphics')
|
||||
requires('javafx.controls')
|
||||
}
|
||||
module('rxjavafx-2.2.2.jar', 'io.reactivex.rxjava2fx', '2.2.2') {
|
||||
module('io.reactivex.rxjava2:rxjavafx', 'io.reactivex.rxjava2fx') {
|
||||
exports('io.reactivex.rxjavafx.schedulers')
|
||||
requires('io.reactivex.rxjava2')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('wellbehavedfx-0.3.3.jar', 'org.fxmisc.wellbehaved', '0.3.3') {
|
||||
module('org.flywaydb:flyway-core', 'org.flywaydb.core') {
|
||||
exports('org.flywaydb.core')
|
||||
exports('org.flywaydb.core.api')
|
||||
exports('org.flywaydb.core.api.exception')
|
||||
exports('org.flywaydb.core.api.configuration')
|
||||
uses('org.flywaydb.core.extensibility.Plugin')
|
||||
requires('java.sql')
|
||||
}
|
||||
module('org.fxmisc.wellbehaved:wellbehavedfx', 'org.fxmisc.wellbehaved') {
|
||||
requires('javafx.base')
|
||||
requires('javafx.graphics')
|
||||
}
|
||||
module('jai-imageio-core-1.4.0.jar', 'com.github.jai.imageio.jai.imageio.core', '1.4.0')
|
||||
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
|
||||
exports('com.sparrowwallet.hummingbird')
|
||||
exports('com.sparrowwallet.hummingbird.registry')
|
||||
requires('co.nstant.in.cbor')
|
||||
module('com.github.jai-imageio:jai-imageio-core', 'com.github.jai.imageio.jai.imageio.core') {
|
||||
requires('java.desktop')
|
||||
}
|
||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||
module('co.nstant.in:cbor', 'co.nstant.in.cbor') {
|
||||
exports('co.nstant.in.cbor')
|
||||
exports('co.nstant.in.cbor.model')
|
||||
exports('co.nstant.in.cbor.builder')
|
||||
}
|
||||
module('commons-codec-1.10.jar', 'commons.codec', '1.10') {
|
||||
exports('org.apache.commons.codec')
|
||||
}
|
||||
module('logback-core-1.2.13.jar', 'ch.qos.logback.core', '1.2.13') {
|
||||
exports('ch.qos.logback.core')
|
||||
}
|
||||
module('jackson-datatype-jsr310-2.13.2.jar', 'jackson-datatype-jsr310', '2.13.2') {
|
||||
exports('com.fasterxml.jackson.datatype.jsr310')
|
||||
}
|
||||
module('json-20240205.jar', 'org.json', '20240205') {
|
||||
exports('org.json')
|
||||
}
|
||||
module('scrypt-1.4.0.jar', 'scrypt', '1.4.0') {
|
||||
exports('com.lambdaworks.codec')
|
||||
exports('com.lambdaworks.crypto')
|
||||
}
|
||||
module('okio-1.6.0.jar', 'com.squareup.okio', '1.6.0') {
|
||||
exports('okio')
|
||||
}
|
||||
module('java-jwt-3.8.1.jar', 'com.auth0.jwt', '3.8.1') {
|
||||
exports('com.auth0.jwt')
|
||||
}
|
||||
module('streamsupport-1.7.0.jar', 'net.sourceforge.streamsupport', '1.7.0') {
|
||||
requires('jdk.unsupported')
|
||||
exports('java8.util')
|
||||
exports('java8.util.function')
|
||||
exports('java8.util.stream')
|
||||
}
|
||||
module('commons-text-1.2.jar', 'org.apache.commons.text', '1.2') {
|
||||
exports('org.apache.commons.text')
|
||||
}
|
||||
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
|
||||
exports('net.jcip.annotations')
|
||||
}
|
||||
module('thumbnailator-0.4.18.jar', 'net.coobird.thumbnailator', '0.4.18') {
|
||||
exports('net.coobird.thumbnailator')
|
||||
requires('java.desktop')
|
||||
}
|
||||
module('fxsvgimage-1.0b2.jar', 'com.github.hervegirod', '1.0b2') {
|
||||
exports('org.girod.javafx.svgimage')
|
||||
requires('javafx.graphics')
|
||||
requires('java.xml')
|
||||
}
|
||||
module("kmp-tor-jvm-${vKmpTor}.jar", 'kmp.tor.jvm', "${vTor}-${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor')
|
||||
requires('kmp.tor.binary.extract.jvm')
|
||||
requires('kmp.tor.manager.jvm')
|
||||
requires('kmp.tor.manager.common.jvm')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.coroutines.core')
|
||||
requires('java.management')
|
||||
}
|
||||
if(kmpOs == "linux" && kmpArch == "arm64") {
|
||||
module("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") {
|
||||
exports("io.matthewnelson.kmp.tor.resource.${kmpOs}.${kmpArch}")
|
||||
exports("kmptor.${kmpOs}.${kmpArch}")
|
||||
}
|
||||
} else {
|
||||
module("kmp-tor-binary-${kmpOs}${kmpArch}-jvm-${vTor}.jar", "kmp.tor.binary.${kmpOs}${kmpArch}", "${vTor}") {
|
||||
exports("io.matthewnelson.kmp.tor.binary.${kmpOs}.${kmpArch}")
|
||||
exports("kmptor.${kmpOs}.${kmpArch}")
|
||||
}
|
||||
}
|
||||
module("kmp-tor-binary-extract-jvm-${vTor}.jar", 'kmp.tor.binary.extract.jvm', "${vTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.binary.extract')
|
||||
exports('io.matthewnelson.kmp.tor.binary.extract.internal')
|
||||
requires('kotlin.stdlib')
|
||||
requires("kmp.tor.binary.${kmpOs}${kmpArch}")
|
||||
requires('kmp.tor.binary.geoip.jvm')
|
||||
}
|
||||
module("kmp-tor-manager-jvm-${vKmpTor}.jar", 'kmp.tor.manager.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.manager')
|
||||
exports('io.matthewnelson.kmp.tor.manager.util')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kmp.tor.manager.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.coroutines.core')
|
||||
requires('kotlinx.atomicfu')
|
||||
requires('kmp.tor.controller.jvm')
|
||||
requires('kmp.tor.common.jvm')
|
||||
}
|
||||
module("kmp-tor-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.manager.common.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.manager.common')
|
||||
exports('io.matthewnelson.kmp.tor.manager.common.event')
|
||||
exports('io.matthewnelson.kmp.tor.manager.common.state')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kmp.tor.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
}
|
||||
module("kmp-tor-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.controller.common.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.config')
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.file')
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.control')
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.control.usecase')
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.events')
|
||||
exports('io.matthewnelson.kmp.tor.controller.common.exceptions')
|
||||
requires('kmp.tor.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.atomicfu')
|
||||
}
|
||||
module("kmp-tor-common-jvm-${vKmpTor}.jar", 'kmp.tor.common.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.common.address')
|
||||
requires('parcelize.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
}
|
||||
module("kmp-tor-controller-jvm-${vKmpTor}.jar", 'kmp.tor.controller.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.controller.internal.controller')
|
||||
requires('kmp.tor.common.jvm')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kotlinx.coroutines.core')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.atomicfu')
|
||||
requires('encoding.core.jvm')
|
||||
requires('encoding.base16.jvm')
|
||||
}
|
||||
module("kmp-tor-ext-callback-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.common.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.common')
|
||||
}
|
||||
module("kmp-tor-ext-callback-manager-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.manager')
|
||||
requires('kmp.tor.manager.jvm')
|
||||
requires('kmp.tor.ext.callback.common.jvm')
|
||||
requires('kmp.tor.ext.callback.manager.common.jvm')
|
||||
requires('kmp.tor.ext.callback.controller.common.jvm')
|
||||
requires('kmp.tor.manager.common.jvm')
|
||||
requires('kmp.tor.controller.common.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
requires('kotlinx.coroutines.core')
|
||||
}
|
||||
module("kmp-tor-ext-callback-manager-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.manager.common.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.manager.common')
|
||||
requires('kmp.tor.ext.callback.controller.common.jvm')
|
||||
}
|
||||
module("kmp-tor-ext-callback-controller-common-jvm-${vKmpTor}.jar", 'kmp.tor.ext.callback.controller.common.jvm', "${vKmpTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control')
|
||||
exports('io.matthewnelson.kmp.tor.ext.callback.controller.common.control.usecase')
|
||||
}
|
||||
module("kmp-tor-binary-geoip-jvm-${vTor}.jar", 'kmp.tor.binary.geoip.jvm', "${vTor}") {
|
||||
exports('io.matthewnelson.kmp.tor.binary.geoip')
|
||||
exports('kmptor')
|
||||
}
|
||||
module("base16-jvm-2.0.0.jar", 'encoding.base16.jvm', "2.0.0") {
|
||||
exports('io.matthewnelson.encoding.base16')
|
||||
requires('encoding.core.jvm')
|
||||
requires('kotlin.stdlib')
|
||||
}
|
||||
module("base32-jvm-2.0.0.jar", 'encoding.base32.jvm', "2.0.0")
|
||||
module("base64-jvm-2.0.0.jar", 'encoding.base64.jvm', "2.0.0")
|
||||
module("core-jvm-2.0.0.jar", 'encoding.core.jvm', "2.0.0") {
|
||||
exports('io.matthewnelson.encoding.core')
|
||||
requires('kotlin.stdlib')
|
||||
}
|
||||
module("parcelize-jvm-0.1.2.jar", 'parcelize.jvm', "0.1.2") {
|
||||
exports('io.matthewnelson.component.parcelize')
|
||||
}
|
||||
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
|
||||
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
|
||||
module('org.jcommander:jcommander', 'org.jcommander') {
|
||||
exports('com.beust.jcommander')
|
||||
}
|
||||
module('jzlib-1.1.3.jar', 'com.jcraft.jzlib', '1.1.3') {
|
||||
module('com.sparrowwallet:hid4java', 'org.hid4java') {
|
||||
requires('com.sun.jna')
|
||||
exports('org.hid4java')
|
||||
exports('org.hid4java.jna')
|
||||
}
|
||||
module('com.sparrowwallet:usb4java', 'org.usb4java') {
|
||||
exports('org.usb4java')
|
||||
}
|
||||
module('com.jcraft:jzlib', 'com.jcraft.jzlib') {
|
||||
exports('com.jcraft.jzlib')
|
||||
}
|
||||
}
|
||||
|
||||
kmpTorResourceFilterJar {
|
||||
keepTorCompilation("current","current")
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
plugins {
|
||||
id 'java-gradle-plugin' // so we can assign and ID to our plugin
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.3'
|
||||
implementation 'org.javamodularity:moduleplugin:1.8.14'
|
||||
implementation 'org.ow2.asm:asm:9.6'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
}
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
// here we register our plugin with an ID
|
||||
register("extra-java-module-info") {
|
||||
id = "extra-java-module-info"
|
||||
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
|
||||
}
|
||||
register("org-openjfx-javafxplugin") {
|
||||
id = "org-openjfx-javafxplugin"
|
||||
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.attributes.Attribute;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
|
||||
/**
|
||||
* Entry point of our plugin that should be applied in the root project.
|
||||
*/
|
||||
public class ExtraModuleInfoPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
|
||||
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
|
||||
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
|
||||
|
||||
// setup the transform for all projects in the build
|
||||
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
|
||||
}
|
||||
|
||||
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
|
||||
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
|
||||
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
|
||||
|
||||
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
|
||||
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
|
||||
c -> c.getAttributes().attribute(javaModule, true));
|
||||
|
||||
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
|
||||
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
|
||||
|
||||
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
|
||||
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
|
||||
t.parameters(p -> {
|
||||
p.setModuleInfo(extension.getModuleInfo());
|
||||
p.setAutomaticModules(extension.getAutomaticModules());
|
||||
});
|
||||
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
|
||||
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
|
||||
if (!configuration.isCanBeResolved()) {
|
||||
return false;
|
||||
}
|
||||
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
||||
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
||||
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
|
||||
import org.gradle.api.Action;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A data class to collect all the module information we want to add.
|
||||
* Here the class is used as extension that can be configured in the build script
|
||||
* and as input to the ExtraModuleInfoTransform that add the information to Jars.
|
||||
*/
|
||||
public class ExtraModuleInfoPluginExtension {
|
||||
|
||||
private final Map<String, ModuleInfo> moduleInfo = new HashMap<>();
|
||||
private final Map<String, String> automaticModules = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Add full module information for a given Jar file.
|
||||
*/
|
||||
public void module(String jarName, String moduleName, String moduleVersion) {
|
||||
module(jarName, moduleName, moduleVersion, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add full module information, including exported packages and dependencies, for a given Jar file.
|
||||
*/
|
||||
public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action<? super ModuleInfo> conf) {
|
||||
ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion);
|
||||
if (conf != null) {
|
||||
conf.execute(moduleInfo);
|
||||
}
|
||||
this.moduleInfo.put(jarName, moduleInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add only an automatic module name to a given jar file.
|
||||
*/
|
||||
public void automaticModule(String jarName, String moduleName) {
|
||||
automaticModules.put(jarName, moduleName);
|
||||
}
|
||||
|
||||
protected Map<String, ModuleInfo> getModuleInfo() {
|
||||
return moduleInfo;
|
||||
}
|
||||
|
||||
protected Map<String, String> getAutomaticModules() {
|
||||
return automaticModules;
|
||||
}
|
||||
}
|
||||
-176
@@ -1,176 +0,0 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import org.gradle.api.artifacts.transform.InputArtifact;
|
||||
import org.gradle.api.artifacts.transform.TransformAction;
|
||||
import org.gradle.api.artifacts.transform.TransformOutputs;
|
||||
import org.gradle.api.artifacts.transform.TransformParameters;
|
||||
import org.gradle.api.file.FileSystemLocation;
|
||||
import org.gradle.api.provider.Provider;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.ModuleVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.jar.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
/**
|
||||
* An artifact transform that applies additional information to Jars without module information.
|
||||
* The transformation fails the build if a Jar does not contain information and no extra information
|
||||
* was defined for it. This way we make sure that all Jars are turned into modules.
|
||||
*/
|
||||
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
|
||||
|
||||
public static class Parameter implements TransformParameters, Serializable {
|
||||
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
|
||||
private Map<String, String> automaticModules = Collections.emptyMap();
|
||||
|
||||
@Input
|
||||
public Map<String, ModuleInfo> getModuleInfo() {
|
||||
return moduleInfo;
|
||||
}
|
||||
|
||||
@Input
|
||||
public Map<String, String> getAutomaticModules() {
|
||||
return automaticModules;
|
||||
}
|
||||
|
||||
public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
|
||||
this.moduleInfo = moduleInfo;
|
||||
}
|
||||
|
||||
public void setAutomaticModules(Map<String, String> automaticModules) {
|
||||
this.automaticModules = automaticModules;
|
||||
}
|
||||
}
|
||||
|
||||
@InputArtifact
|
||||
protected abstract Provider<FileSystemLocation> getInputArtifact();
|
||||
|
||||
@Override
|
||||
public void transform(TransformOutputs outputs) {
|
||||
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
|
||||
Map<String, String> automaticModules = getParameters().automaticModules;
|
||||
File originalJar = getInputArtifact().get().getAsFile();
|
||||
String originalJarName = originalJar.getName();
|
||||
|
||||
//Recreate jackson jars as open, non-synthetic modules
|
||||
if ((isModule(originalJar) && !originalJarName.contains("jackson")) || originalJarName.startsWith("javafx-")) {
|
||||
outputs.file(originalJar);
|
||||
} else if (moduleInfo.containsKey(originalJarName)) {
|
||||
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
|
||||
} else if (isAutoModule(originalJar)) {
|
||||
outputs.file(originalJar);
|
||||
} else if (automaticModules.containsKey(originalJarName)) {
|
||||
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
|
||||
} else if(originalJarName.startsWith("kotlin-stdlib-common")) {
|
||||
//ignore
|
||||
} else {
|
||||
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isModule(File jar) {
|
||||
Pattern moduleInfoClassMrjarPath = Pattern.compile("META-INF/versions/\\d+/module-info.class");
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
|
||||
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
|
||||
ZipEntry next = inputStream.getNextEntry();
|
||||
while (next != null) {
|
||||
if ("module-info.class".equals(next.getName())) {
|
||||
return true;
|
||||
}
|
||||
if (isMultiReleaseJar && moduleInfoClassMrjarPath.matcher(next.getName()).matches()) {
|
||||
return true;
|
||||
}
|
||||
next = inputStream.getNextEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean containsMultiReleaseJarEntry(JarInputStream jarStream) {
|
||||
Manifest manifest = jarStream.getManifest();
|
||||
return manifest != null && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"));
|
||||
}
|
||||
|
||||
private boolean isAutoModule(File jar) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
|
||||
return inputStream.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private File getModuleJar(TransformOutputs outputs, File originalJar) {
|
||||
return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-module.jar");
|
||||
}
|
||||
|
||||
private static void addAutomaticModuleName(File originalJar, File moduleJar, String moduleName) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
|
||||
Manifest manifest = inputStream.getManifest();
|
||||
manifest.getMainAttributes().put(new Attributes.Name("Automatic-Module-Name"), moduleName);
|
||||
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
|
||||
copyEntries(inputStream, outputStream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
|
||||
Manifest manifest = inputStream.getManifest();
|
||||
if(manifest == null) {
|
||||
manifest = new Manifest();
|
||||
}
|
||||
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), manifest)) {
|
||||
copyEntries(inputStream, outputStream);
|
||||
outputStream.putNextEntry(new JarEntry("module-info.class"));
|
||||
outputStream.write(addModuleInfo(moduleInfo));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
|
||||
JarEntry jarEntry = inputStream.getNextJarEntry();
|
||||
while (jarEntry != null) {
|
||||
if(!jarEntry.getName().equals("module-info.class")) {
|
||||
outputStream.putNextEntry(jarEntry);
|
||||
outputStream.write(inputStream.readAllBytes());
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
jarEntry = inputStream.getNextJarEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] addModuleInfo(ModuleInfo moduleInfo) {
|
||||
ClassWriter classWriter = new ClassWriter(0);
|
||||
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
|
||||
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.getModuleVersion());
|
||||
for (String packageName : moduleInfo.getExports()) {
|
||||
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
|
||||
}
|
||||
moduleVisitor.visitRequire("java.base", 0, null);
|
||||
for (String requireName : moduleInfo.getRequires()) {
|
||||
moduleVisitor.visitRequire(requireName, 0, null);
|
||||
}
|
||||
for (String requireName : moduleInfo.getRequiresTransitive()) {
|
||||
moduleVisitor.visitRequire(requireName, Opcodes.ACC_TRANSITIVE, null);
|
||||
}
|
||||
for (String usesName : moduleInfo.getUses()) {
|
||||
moduleVisitor.visitUse(usesName.replace('.', '/'));
|
||||
}
|
||||
moduleVisitor.visitEnd();
|
||||
classWriter.visitEnd();
|
||||
return classWriter.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data class to hold the information that should be added as module-info.class to an existing Jar file.
|
||||
*/
|
||||
public class ModuleInfo implements Serializable {
|
||||
private String moduleName;
|
||||
private String moduleVersion;
|
||||
private List<String> exports = new ArrayList<>();
|
||||
private List<String> requires = new ArrayList<>();
|
||||
private List<String> requiresTransitive = new ArrayList<>();
|
||||
private List<String> uses = new ArrayList<>();
|
||||
|
||||
ModuleInfo(String moduleName, String moduleVersion) {
|
||||
this.moduleName = moduleName;
|
||||
this.moduleVersion = moduleVersion;
|
||||
}
|
||||
|
||||
public void exports(String exports) {
|
||||
this.exports.add(exports);
|
||||
}
|
||||
|
||||
public void requires(String requires) {
|
||||
this.requires.add(requires);
|
||||
}
|
||||
|
||||
public void requiresTransitive(String requiresTransitive) {
|
||||
this.requiresTransitive.add(requiresTransitive);
|
||||
}
|
||||
|
||||
public void uses(String uses) {
|
||||
this.uses.add(uses);
|
||||
}
|
||||
|
||||
public String getModuleName() {
|
||||
return moduleName;
|
||||
}
|
||||
|
||||
protected String getModuleVersion() {
|
||||
return moduleVersion;
|
||||
}
|
||||
|
||||
protected List<String> getExports() {
|
||||
return exports;
|
||||
}
|
||||
|
||||
protected List<String> getRequires() {
|
||||
return requires;
|
||||
}
|
||||
|
||||
protected List<String> getRequiresTransitive() {
|
||||
return requiresTransitive;
|
||||
}
|
||||
|
||||
protected List<String> getUses() {
|
||||
return uses;
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* 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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* 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 )
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* 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()));
|
||||
}
|
||||
}
|
||||
+20
-46
@@ -12,62 +12,36 @@ Work on resolving both of these issues is ongoing.
|
||||
### Install Java
|
||||
|
||||
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
|
||||
For v1.6.6 and later, this is Eclipse Temurin 18.0.1+10.
|
||||
For v1.6.6 to v1.9.1, this was Eclipse Temurin 18.0.1+10. For v2.0.0 to v2.3.1, this was Eclipse Temurin 22.0.2+9. For v2.4.0 and later, Eclipse Temurin 25.0.2+10 is used.
|
||||
|
||||
Note: Do not install Java using a system package manager (e.g. apt, dnf, rpm).
|
||||
Linux packages replace the JDK's bundled `cacerts` file with a symlink to the system CA certificates, which differ from those in the release tarballs and will produce a non-reproducible build.
|
||||
|
||||
#### Java from Adoptium github repo
|
||||
|
||||
It is available for all supported platforms from [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
|
||||
It is available for all supported platforms from [Eclipse Temurin 25.0.2+10](https://github.com/adoptium/temurin25-binaries/releases/tag/jdk-25.0.2%2B10).
|
||||
|
||||
For reference, the downloads are as follows:
|
||||
- [Linux x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz)
|
||||
- [MacOS x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_mac_hotspot_18.0.1_10.tar.gz)
|
||||
- [MacOS aarch64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_aarch64_mac_hotspot_18.0.1_10.tar.gz)
|
||||
- [Windows x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_windows_hotspot_18.0.1_10.zip)
|
||||
- [Linux x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz)
|
||||
- [Linux aarch64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_linux_hotspot_25.0.2_10.tar.gz)
|
||||
- [MacOS x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_mac_hotspot_25.0.2_10.tar.gz)
|
||||
- [MacOS aarch64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_mac_hotspot_25.0.2_10.tar.gz)
|
||||
- [Windows x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_windows_hotspot_25.0.2_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:
|
||||
|
||||
- Install dependencies:
|
||||
```sh
|
||||
sudo apt-get install -y wget curl apt-transport-https gnupg
|
||||
On Linux, extract the tarball and set `JAVA_HOME` to use it for the build:
|
||||
```shell
|
||||
tar -xzf OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz
|
||||
export JAVA_HOME=$PWD/jdk-25.0.2+10
|
||||
export PATH=$JAVA_HOME/bin:$PATH
|
||||
```
|
||||
|
||||
Download Adoptium public PGP key:
|
||||
```sh
|
||||
curl --tlsv1.2 --proto =https --location -o adoptium.asc https://packages.adoptium.net/artifactory/api/gpg/key/public
|
||||
```
|
||||
#### Java from SDKMAN
|
||||
|
||||
Check if key fingerprint matches: `3B04D753C9050D9A5D343F39843C48A565F8F04B`:
|
||||
```
|
||||
gpg --import --import-options show-only adoptium.asc
|
||||
```
|
||||
If key doesn't match, do not proceed.
|
||||
|
||||
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).
|
||||
An 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 18.0.1-tem
|
||||
sdk install java 25.0.2-tem
|
||||
```
|
||||
|
||||
### Other requirements
|
||||
@@ -82,7 +56,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||
|
||||
```shell
|
||||
GIT_TAG="1.9.0"
|
||||
GIT_TAG="2.5.0"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
@@ -100,7 +74,7 @@ git checkout "${GIT_TAG}"
|
||||
```
|
||||
|
||||
Note - there is an additional step if you updated rather than initially cloned your repo at `GIT_TAG`.
|
||||
This is due to the [drongo submodule](https://github.com/sparrowwallet/drongo/tree/master) which needs to be checked out to the commit state it had at the time of the release.
|
||||
This is due to the Git submodules which need to be checked out to the commit state they had at the time of the release.
|
||||
Only then your build will be comparable to the provided one in the release section of Github.
|
||||
To checkout the submodule to the correct commit for `GIT_TAG`, additionally run:
|
||||
|
||||
|
||||
+1
-1
Submodule drongo updated: 571db32574...3779317388
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
#
|
||||
# Copyright © 2015-2021 the original authors.
|
||||
# Copyright © 2015 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.
|
||||
@@ -15,6 +15,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
@@ -55,7 +57,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -84,7 +86,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -112,7 +114,6 @@ case "$( uname )" in #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
@@ -170,7 +171,6 @@ fi
|
||||
# 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" )
|
||||
|
||||
@@ -203,15 +203,14 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
Vendored
+13
-12
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
Submodule
+1
Submodule lark added at 2a8c73c131
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
set -e # Exit on any error
|
||||
|
||||
# Define paths
|
||||
BUILD_DIR="build"
|
||||
JPACKAGE_DIR="$BUILD_DIR/jpackage"
|
||||
TEMP_DIR="$BUILD_DIR/repackage"
|
||||
|
||||
# Find the .deb file in build/jpackage (assuming there is only one)
|
||||
DEB_FILE=$(find "$JPACKAGE_DIR" -type f -name "*.deb" -print -quit)
|
||||
|
||||
# Check if a .deb file was found
|
||||
if [ -z "$DEB_FILE" ]; then
|
||||
echo "Error: No .deb file found in $JPACKAGE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the filename from the path for later use
|
||||
DEB_FILENAME=$(basename "$DEB_FILE")
|
||||
|
||||
echo "Found .deb file: $DEB_FILENAME"
|
||||
|
||||
# Create a temp directory inside build to avoid file conflicts
|
||||
mkdir -p "$TEMP_DIR"
|
||||
cd "$TEMP_DIR"
|
||||
|
||||
# Extract the .deb file contents
|
||||
ar x "../../$DEB_FILE"
|
||||
|
||||
# Decompress zst files to tar
|
||||
unzstd control.tar.zst
|
||||
unzstd data.tar.zst
|
||||
|
||||
# Compress tar files to xz
|
||||
xz -c control.tar > control.tar.xz
|
||||
xz -c data.tar > data.tar.xz
|
||||
|
||||
# Remove the original .deb file
|
||||
rm "../../$DEB_FILE"
|
||||
|
||||
# Create the new .deb file with xz compression in the original location
|
||||
ar cr "../../$DEB_FILE" debian-binary control.tar.xz data.tar.xz
|
||||
|
||||
# Clean up temp files
|
||||
cd ../..
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo "Repackaging complete: $DEB_FILENAME"
|
||||
@@ -1,3 +1,4 @@
|
||||
rootProject.name = 'sparrow'
|
||||
include 'drongo'
|
||||
include 'lark'
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
mime-type=x-scheme-handler/auth47
|
||||
description=Auth47 Authentication URI
|
||||
@@ -1,2 +0,0 @@
|
||||
mime-type=x-scheme-handler/bitcoin
|
||||
description=Bitcoin Scheme URI
|
||||
@@ -1,2 +0,0 @@
|
||||
mime-type=x-scheme-handler/lightning
|
||||
description=LNURL URI
|
||||
@@ -0,0 +1,12 @@
|
||||
Package: sparrowserver
|
||||
Version: ${version}-1
|
||||
Section: utils
|
||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||
Priority: optional
|
||||
Architecture: ${arch}
|
||||
Conflicts: sparrow (<= 2.1.4)
|
||||
Replaces: sparrow (<= 2.1.4)
|
||||
Provides: sparrowserver
|
||||
Description: Sparrow Server
|
||||
Depends: libc6, zlib1g
|
||||
Installed-Size: ${size}
|
||||
@@ -0,0 +1,85 @@
|
||||
Summary: Sparrow Server
|
||||
Name: sparrowserver
|
||||
Version: ${version}
|
||||
Release: 1
|
||||
License: ASL 2.0
|
||||
Vendor: Unknown
|
||||
|
||||
%if "x" != "x"
|
||||
URL: https://sparrowwallet.com
|
||||
%endif
|
||||
|
||||
%if "x/opt" != "x"
|
||||
Prefix: /opt
|
||||
%endif
|
||||
|
||||
Provides: sparrowserver
|
||||
Obsoletes: sparrow <= 2.1.4
|
||||
|
||||
%if "xutils" != "x"
|
||||
Group: utils
|
||||
%endif
|
||||
|
||||
Autoprov: 0
|
||||
Autoreq: 0
|
||||
|
||||
#comment line below to enable effective jar compression
|
||||
#it could easily get your package size from 40 to 15Mb but
|
||||
#build time will substantially increase and it may require unpack200/system java to install
|
||||
%define __jar_repack %{nil}
|
||||
|
||||
# on RHEL we got unwanted improved debugging enhancements
|
||||
%define _build_id_links none
|
||||
|
||||
%define package_filelist %{_builddir}/%{name}.files
|
||||
%define app_filelist %{_builddir}/%{name}.app.files
|
||||
%define filesystem_filelist %{_builddir}/%{name}.filesystem.files
|
||||
|
||||
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
|
||||
|
||||
%description
|
||||
Sparrow Server
|
||||
|
||||
%global __os_install_post %{nil}
|
||||
|
||||
%prep
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
install -d -m 755 %{buildroot}/opt/sparrowserver
|
||||
cp -r %{_sourcedir}/opt/sparrowserver/* %{buildroot}/opt/sparrowserver
|
||||
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
||||
install -d -m 755 %{buildroot}/lib/systemd/system
|
||||
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
||||
fi
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
%define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE}
|
||||
install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}"
|
||||
install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}"
|
||||
%endif
|
||||
(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist}
|
||||
{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist}
|
||||
comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist}
|
||||
sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
||||
(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist}
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist}
|
||||
%endif
|
||||
|
||||
%files -f %{package_filelist}
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
%license "%{license_install_file}"
|
||||
%endif
|
||||
|
||||
%post
|
||||
package_type=rpm
|
||||
|
||||
%pre
|
||||
package_type=rpm
|
||||
|
||||
%preun
|
||||
package_type=rpm
|
||||
|
||||
%clean
|
||||
@@ -1,10 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Name=Sparrow
|
||||
Comment=Sparrow
|
||||
Exec=/opt/sparrow/bin/Sparrow %U
|
||||
Icon=/opt/sparrow/lib/Sparrow.png
|
||||
Exec=/opt/sparrowwallet/bin/Sparrow %U
|
||||
Icon=/opt/sparrowwallet/lib/Sparrow.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Finance;Network;
|
||||
MimeType=application/psbt;application/bitcoin-transaction;application/pgp-signature;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||
StartupWMClass=Sparrow
|
||||
SingleMainWindow=true
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
Package: sparrowwallet
|
||||
Version: ${version}-1
|
||||
Section: utils
|
||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||
Priority: optional
|
||||
Architecture: ${arch}
|
||||
Provides: sparrowwallet
|
||||
Conflicts: sparrow (<= 2.1.4)
|
||||
Replaces: sparrow (<= 2.1.4)
|
||||
Description: Sparrow Wallet
|
||||
Depends: libasound2, libbsd0, libc6, libmd0, libx11-6, libxau6, libxcb1, libxdmcp6, libxext6, libxi6, libxrender1, libxtst6, xdg-utils
|
||||
Installed-Size: ${size}
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
# postinst script for sparrowwallet
|
||||
#
|
||||
# see: dh_installdeb(1)
|
||||
|
||||
set -e
|
||||
|
||||
# summary of how this script can be called:
|
||||
# * <postinst> `configure' <most-recently-configured-version>
|
||||
# * <old-postinst> `abort-upgrade' <new version>
|
||||
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
|
||||
# <new-version>
|
||||
# * <postinst> `abort-remove'
|
||||
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
|
||||
# <failed-install-package> <version> `removing'
|
||||
# <conflicting-package> <version>
|
||||
# for details, see https://www.debian.org/doc/debian-policy/ or
|
||||
# the debian-policy package
|
||||
|
||||
package_type=deb
|
||||
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
if ! getent group plugdev > /dev/null; then
|
||||
groupadd -r plugdev
|
||||
fi
|
||||
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
|
||||
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
|
||||
fi
|
||||
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
|
||||
/bin/udevadm control --reload
|
||||
/bin/udevadm trigger
|
||||
fi
|
||||
;;
|
||||
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "postinst called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||
<mime-type type="application/psbt">
|
||||
<comment>Partially Signed Bitcoin Transaction</comment>
|
||||
<glob pattern="*.psbt"/>
|
||||
</mime-type>
|
||||
<mime-type type="application/bitcoin-transaction">
|
||||
<comment>Bitcoin Transaction</comment>
|
||||
<glob pattern="*.txn"/>
|
||||
</mime-type>
|
||||
<mime-type type="application/pgp-signature">
|
||||
<comment>ASCII Armored File</comment>
|
||||
<glob pattern="*.asc"/>
|
||||
</mime-type>
|
||||
<mime-type type="x-scheme-handler/bitcoin">
|
||||
<comment>Bitcoin Scheme URI</comment>
|
||||
</mime-type>
|
||||
<mime-type type="x-scheme-handler/auth47">
|
||||
<comment>Auth47 Authentication URI</comment>
|
||||
</mime-type>
|
||||
<mime-type type="x-scheme-handler/lightning">
|
||||
<comment>LNURL URI</comment>
|
||||
</mime-type>
|
||||
</mime-info>
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
Summary: Sparrow
|
||||
Name: sparrowwallet
|
||||
Version: ${version}
|
||||
Release: 1
|
||||
License: ASL 2.0
|
||||
Vendor: Unknown
|
||||
|
||||
%if "x" != "x"
|
||||
URL: https://sparrowwallet.com
|
||||
%endif
|
||||
|
||||
%if "x/opt" != "x"
|
||||
Prefix: /opt
|
||||
%endif
|
||||
|
||||
Provides: sparrowwallet
|
||||
Obsoletes: sparrow <= 2.1.4
|
||||
|
||||
%if "xutils" != "x"
|
||||
Group: utils
|
||||
%endif
|
||||
|
||||
Autoprov: 0
|
||||
Autoreq: 0
|
||||
%if "xxdg-utils" != "x" || "x" != "x"
|
||||
Requires: xdg-utils
|
||||
%endif
|
||||
|
||||
#comment line below to enable effective jar compression
|
||||
#it could easily get your package size from 40 to 15Mb but
|
||||
#build time will substantially increase and it may require unpack200/system java to install
|
||||
%define __jar_repack %{nil}
|
||||
|
||||
# on RHEL we got unwanted improved debugging enhancements
|
||||
%define _build_id_links none
|
||||
|
||||
%define package_filelist %{_builddir}/%{name}.files
|
||||
%define app_filelist %{_builddir}/%{name}.app.files
|
||||
%define filesystem_filelist %{_builddir}/%{name}.filesystem.files
|
||||
|
||||
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
|
||||
|
||||
%description
|
||||
Sparrow Wallet
|
||||
|
||||
%global __os_install_post %{nil}
|
||||
|
||||
%prep
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
install -d -m 755 %{buildroot}/opt/sparrowwallet
|
||||
cp -r %{_sourcedir}/opt/sparrowwallet/* %{buildroot}/opt/sparrowwallet
|
||||
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
||||
install -d -m 755 %{buildroot}/lib/systemd/system
|
||||
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
||||
fi
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
%define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE}
|
||||
install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}"
|
||||
install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}"
|
||||
%endif
|
||||
(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist}
|
||||
{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist}
|
||||
comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist}
|
||||
sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
||||
(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist}
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist}
|
||||
%endif
|
||||
|
||||
%files -f %{package_filelist}
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
%license "%{license_install_file}"
|
||||
%endif
|
||||
|
||||
%post
|
||||
package_type=rpm
|
||||
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
if ! getent group plugdev > /dev/null; then
|
||||
groupadd -r plugdev
|
||||
fi
|
||||
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
|
||||
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
|
||||
fi
|
||||
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
|
||||
/bin/udevadm control --reload
|
||||
/bin/udevadm trigger
|
||||
fi
|
||||
|
||||
%pre
|
||||
package_type=rpm
|
||||
file_belongs_to_single_package ()
|
||||
{
|
||||
if [ ! -e "$1" ]; then
|
||||
false
|
||||
elif [ "$package_type" = rpm ]; then
|
||||
test `rpm -q --whatprovides "$1" | wc -l` = 1
|
||||
elif [ "$package_type" = deb ]; then
|
||||
test `dpkg -S "$1" | wc -l` = 1
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
do_if_file_belongs_to_single_package ()
|
||||
{
|
||||
local file="$1"
|
||||
shift
|
||||
|
||||
if file_belongs_to_single_package "$file"; then
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$1" -gt 1 ]; then
|
||||
:;
|
||||
fi
|
||||
|
||||
%preun
|
||||
package_type=rpm
|
||||
file_belongs_to_single_package ()
|
||||
{
|
||||
if [ ! -e "$1" ]; then
|
||||
false
|
||||
elif [ "$package_type" = rpm ]; then
|
||||
test `rpm -q --whatprovides "$1" | wc -l` = 1
|
||||
elif [ "$package_type" = deb ]; then
|
||||
test `dpkg -S "$1" | wc -l` = 1
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
do_if_file_belongs_to_single_package ()
|
||||
{
|
||||
local file="$1"
|
||||
shift
|
||||
|
||||
if file_belongs_to_single_package "$file"; then
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
#
|
||||
# Remove $1 desktop file from the list of default handlers for $2 mime type
|
||||
# in $3 file dumping output to stdout.
|
||||
#
|
||||
desktop_filter_out_default_mime_handler ()
|
||||
{
|
||||
local defaults_list="$3"
|
||||
|
||||
local desktop_file="$1"
|
||||
local mime_type="$2"
|
||||
|
||||
awk -f- "$defaults_list" <<EOF
|
||||
BEGIN {
|
||||
mime_type="$mime_type"
|
||||
mime_type_regexp="~" mime_type "="
|
||||
desktop_file="$desktop_file"
|
||||
}
|
||||
\$0 ~ mime_type {
|
||||
\$0 = substr(\$0, length(mime_type) + 2);
|
||||
split(\$0, desktop_files, ";")
|
||||
remaining_desktop_files
|
||||
counter=0
|
||||
for (idx in desktop_files) {
|
||||
if (desktop_files[idx] != desktop_file) {
|
||||
++counter;
|
||||
}
|
||||
}
|
||||
if (counter) {
|
||||
printf mime_type "="
|
||||
for (idx in desktop_files) {
|
||||
if (desktop_files[idx] != desktop_file) {
|
||||
printf desktop_files[idx]
|
||||
if (--counter) {
|
||||
printf ";"
|
||||
}
|
||||
}
|
||||
}
|
||||
printf "\n"
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
{ print }
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Remove $2 desktop file from the list of default handlers for $@ mime types
|
||||
# in $1 file.
|
||||
# Result is saved in $1 file.
|
||||
#
|
||||
desktop_uninstall_default_mime_handler_0 ()
|
||||
{
|
||||
local defaults_list=$1
|
||||
shift
|
||||
[ -f "$defaults_list" ] || return 0
|
||||
|
||||
local desktop_file="$1"
|
||||
shift
|
||||
|
||||
tmpfile1=$(mktemp)
|
||||
tmpfile2=$(mktemp)
|
||||
cat "$defaults_list" > "$tmpfile1"
|
||||
|
||||
local v
|
||||
local update=
|
||||
for mime in "$@"; do
|
||||
desktop_filter_out_default_mime_handler "$desktop_file" "$mime" "$tmpfile1" > "$tmpfile2"
|
||||
v="$tmpfile2"
|
||||
tmpfile2="$tmpfile1"
|
||||
tmpfile1="$v"
|
||||
|
||||
if ! diff -q "$tmpfile1" "$tmpfile2" > /dev/null; then
|
||||
update=yes
|
||||
desktop_trace Remove $desktop_file default handler for $mime mime type from $defaults_list file
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$update" ]; then
|
||||
cat "$tmpfile1" > "$defaults_list"
|
||||
desktop_trace "$defaults_list" file updated
|
||||
fi
|
||||
|
||||
rm -f "$tmpfile1" "$tmpfile2"
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Remove $1 desktop file from the list of default handlers for $@ mime types
|
||||
# in all known system defaults lists.
|
||||
#
|
||||
desktop_uninstall_default_mime_handler ()
|
||||
{
|
||||
for f in /usr/share/applications/defaults.list /usr/local/share/applications/defaults.list; do
|
||||
desktop_uninstall_default_mime_handler_0 "$f" "$@"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
desktop_trace ()
|
||||
{
|
||||
echo "$@"
|
||||
}
|
||||
|
||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop desktop_uninstall_default_mime_handler sparrowwallet-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning
|
||||
|
||||
|
||||
%clean
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.9.1</string>
|
||||
<string>2.5.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
@@ -33,8 +33,12 @@
|
||||
<string>Copyright (C) 2021</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSCameraUseContinuityCameraDeviceType</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Sparrow requires access to the camera in order to scan QR codes</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>Sparrow requires access to the local network in order to connect to your configured server</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -1,19 +1,16 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.io.ByteSource;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.*;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
||||
@@ -25,8 +22,8 @@ import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.settings.SettingsGroup;
|
||||
import com.sparrowwallet.sparrow.settings.SettingsDialog;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionData;
|
||||
@@ -34,7 +31,7 @@ import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletController;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import de.codecentric.centerdevice.MenuToolkit;
|
||||
import de.jangassen.MenuToolkit;
|
||||
import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -53,12 +50,14 @@ import javafx.geometry.Side;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.*;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.Notifications;
|
||||
import org.controlsfx.control.StatusBar;
|
||||
@@ -73,6 +72,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.text.ParseException;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||
@@ -82,12 +82,15 @@ public class AppController implements Initializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(AppController.class);
|
||||
|
||||
public static final String DRAG_OVER_CLASS = "drag-over";
|
||||
public static final int TAB_LABEL_MAX_WIDTH = 300;
|
||||
public static final double TAB_LABEL_GRAPHIC_OPACITY_INACTIVE = 0.8;
|
||||
public static final double TAB_LABEL_GRAPHIC_OPACITY_ACTIVE = 0.95;
|
||||
public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
|
||||
public static final String CONNECTION_FAILED_PREFIX = "Connection failed: ";
|
||||
public static final String TRYING_ANOTHER_SERVER_MESSAGE = "trying another server...";
|
||||
public static final String JPACKAGE_APP_PATH = "jpackage.app-path";
|
||||
|
||||
@FXML
|
||||
private VBox rootBox;
|
||||
|
||||
@FXML
|
||||
private MenuItem saveTransaction;
|
||||
@@ -137,6 +140,13 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private ToggleGroup unitFormat;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem chunkAddresses;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem hideEmptyUsedAddresses;
|
||||
private static final BooleanProperty hideEmptyUsedAddressesProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private ToggleGroup theme;
|
||||
|
||||
@@ -145,13 +155,16 @@ public class AppController implements Initializable {
|
||||
private static final BooleanProperty openWalletsInNewWindowsProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem hideEmptyUsedAddresses;
|
||||
private static final BooleanProperty hideEmptyUsedAddressesProperty = new SimpleBooleanProperty();
|
||||
private CheckMenuItem hideAmounts;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem useHdCameraResolution;
|
||||
private static final BooleanProperty useHdCameraResolutionProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem mirrorCameraImage;
|
||||
private static final BooleanProperty mirrorCameraImageProperty = new SimpleBooleanProperty();
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem showLoadingLog;
|
||||
private static final BooleanProperty showLoadingLogProperty = new SimpleBooleanProperty();
|
||||
@@ -276,7 +289,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
void initializeView() {
|
||||
setPlatformApplicationMenu();
|
||||
Platform.runLater(this::setPlatformApplicationMenu);
|
||||
|
||||
rootStack.getScene().getWindow().setOnHiding(windowEvent -> {
|
||||
if(searchWalletDialog != null && searchWalletDialog.isShowing()) {
|
||||
@@ -365,6 +378,13 @@ public class AppController implements Initializable {
|
||||
Optional<Toggle> selectedFormatToggle = unitFormat.getToggles().stream().filter(toggle -> selectedFormat.equals(toggle.getUserData())).findFirst();
|
||||
selectedFormatToggle.ifPresent(toggle -> unitFormat.selectToggle(toggle));
|
||||
|
||||
chunkAddresses.setSelected(Config.get().isChunkAddresses());
|
||||
if(Config.get().isChunkAddresses()) {
|
||||
rootBox.getStyleClass().add("chunk-addresses");
|
||||
}
|
||||
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
|
||||
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
|
||||
|
||||
Theme configTheme = Config.get().getTheme();
|
||||
if(configTheme == null) {
|
||||
configTheme = Theme.LIGHT;
|
||||
@@ -377,10 +397,11 @@ public class AppController implements Initializable {
|
||||
|
||||
openWalletsInNewWindowsProperty.set(Config.get().isOpenWalletsInNewWindows());
|
||||
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
|
||||
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
|
||||
hideEmptyUsedAddresses.selectedProperty().bindBidirectional(hideEmptyUsedAddressesProperty);
|
||||
useHdCameraResolutionProperty.set(Config.get().isHdCapture());
|
||||
hideAmounts.setSelected(Config.get().isHideAmounts());
|
||||
useHdCameraResolutionProperty.set(Config.get().getWebcamResolution() == null || Config.get().getWebcamResolution().isWidescreenAspect());
|
||||
useHdCameraResolution.selectedProperty().bindBidirectional(useHdCameraResolutionProperty);
|
||||
mirrorCameraImageProperty.set(Config.get().isMirrorCapture());
|
||||
mirrorCameraImage.selectedProperty().bindBidirectional(mirrorCameraImageProperty);
|
||||
showTxHexProperty.set(Config.get().isShowTransactionHex());
|
||||
showTxHex.selectedProperty().bindBidirectional(showTxHexProperty);
|
||||
showLoadingLogProperty.set(Config.get().isShowLoadingLog());
|
||||
@@ -398,7 +419,7 @@ public class AppController implements Initializable {
|
||||
networkItem.setOnAction(event -> restart(event, network));
|
||||
restart.getItems().add(networkItem);
|
||||
}
|
||||
restart.setVisible(System.getProperty(JPACKAGE_APP_PATH) != null);
|
||||
restart.setVisible(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH) != null);
|
||||
|
||||
saveTransaction.setDisable(true);
|
||||
showTransaction.visibleProperty().bind(Bindings.and(saveTransaction.visibleProperty(), saveTransaction.disableProperty().not()));
|
||||
@@ -433,8 +454,8 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void registerShortcuts() {
|
||||
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
|
||||
if(platform == org.controlsfx.tools.Platform.OSX) {
|
||||
OsType osType = OsType.getCurrent();
|
||||
if(osType == OsType.MACOS) {
|
||||
tabs.getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if(event.isShortcutDown() && event.isAltDown() && (event.getCode() == KeyCode.LEFT || event.getCode() == KeyCode.RIGHT)) {
|
||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||
@@ -449,14 +470,14 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void setPlatformApplicationMenu() {
|
||||
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
|
||||
if(platform == org.controlsfx.tools.Platform.OSX) {
|
||||
OsType osType = OsType.getCurrent();
|
||||
if(osType == OsType.MACOS && Interface.get() == Interface.DESKTOP) {
|
||||
MenuToolkit tk = MenuToolkit.toolkit();
|
||||
MenuItem preferences = new MenuItem("Preferences...");
|
||||
preferences.setOnAction(this::openPreferences);
|
||||
preferences.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN));
|
||||
MenuItem settings = new MenuItem("Settings...");
|
||||
settings.setOnAction(this::openSettings);
|
||||
settings.setAccelerator(new KeyCodeCombination(KeyCode.COMMA, KeyCombination.META_DOWN));
|
||||
Menu defaultApplicationMenu = new Menu("Apple", null, tk.createAboutMenuItem(SparrowWallet.APP_NAME, getAboutStage()), new SeparatorMenuItem(),
|
||||
preferences, new SeparatorMenuItem(),
|
||||
settings, new SeparatorMenuItem(),
|
||||
tk.createHideMenuItem(SparrowWallet.APP_NAME), tk.createHideOthersMenuItem(), tk.createUnhideAllMenuItem(), new SeparatorMenuItem(),
|
||||
tk.createQuitMenuItem(SparrowWallet.APP_NAME));
|
||||
tk.setApplicationMenu(defaultApplicationMenu);
|
||||
@@ -464,11 +485,11 @@ public class AppController implements Initializable {
|
||||
fileMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||
helpMenu.getItems().removeIf(item -> item.getStyleClass().contains("osxHide"));
|
||||
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
} else if(osType == OsType.WINDOWS) {
|
||||
toolsMenu.getItems().removeIf(item -> item.getStyleClass().contains("windowsHide"));
|
||||
}
|
||||
|
||||
if(platform == org.controlsfx.tools.Platform.UNIX || !TrayManager.isSupported()) {
|
||||
if(osType == OsType.UNIX || !TrayManager.isSupported()) {
|
||||
viewMenu.getItems().remove(minimizeToTray);
|
||||
}
|
||||
}
|
||||
@@ -494,7 +515,7 @@ public class AppController implements Initializable {
|
||||
welcomeDialog.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
|
||||
if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) {
|
||||
openPreferences(PreferenceGroup.SERVER);
|
||||
openSettings(SettingsGroup.SERVER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,7 +561,7 @@ public class AppController implements Initializable {
|
||||
StackPane root = loader.load();
|
||||
AboutController controller = loader.getController();
|
||||
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
if(OsType.getCurrent() == OsType.WINDOWS) {
|
||||
root.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||
}
|
||||
|
||||
@@ -568,21 +589,27 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
public void installUdevRules(ActionEvent event) {
|
||||
Hwi.EnumerateService enumerateService = new Hwi.EnumerateService(null);
|
||||
enumerateService.setOnSucceeded(workerStateEvent -> {
|
||||
Platform.runLater(this::showInstallUdevMessage);
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
Platform.runLater(this::showInstallUdevMessage);
|
||||
});
|
||||
enumerateService.start();
|
||||
}
|
||||
String commands = """
|
||||
sudo install -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
sudo udevadm control --reload
|
||||
sudo udevadm trigger
|
||||
sudo groupadd -f -r plugdev
|
||||
sudo usermod -aG plugdev `whoami`
|
||||
""";
|
||||
String home = System.getProperty(SparrowWallet.JPACKAGE_APP_PATH);
|
||||
if(home != null && !home.startsWith("/opt/sparrowwallet") && home.endsWith("bin/Sparrow")) {
|
||||
home = home.replace("bin/Sparrow", "");
|
||||
commands = commands.replace("/opt/sparrowwallet/", home);
|
||||
}
|
||||
|
||||
public void showInstallUdevMessage() {
|
||||
TextAreaDialog dialog = new TextAreaDialog("sudo " + Config.get().getHwi().getAbsolutePath() + " installudevrules", false);
|
||||
TextAreaDialog dialog = new TextAreaDialog(commands, false);
|
||||
dialog.initOwner(rootStack.getScene().getWindow());
|
||||
dialog.setTitle("Install Udev Rules");
|
||||
dialog.getDialogPane().setHeaderText("Installing udev rules ensures devices can connect over USB.\nThis command requires root privileges.\nOpen a shell and enter the following:");
|
||||
dialog.setTitle("Install udev Rules");
|
||||
dialog.getDialogPane().setHeaderText("""
|
||||
Installing udev rules ensures devices can connect over USB.
|
||||
These commands require root privileges.
|
||||
Open a shell and enter the following.
|
||||
""");
|
||||
dialog.showAndWait();
|
||||
}
|
||||
|
||||
@@ -592,7 +619,7 @@ public class AppController implements Initializable {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Transaction");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("PSBT", "*.psbt"),
|
||||
new FileChooser.ExtensionFilter("TXN", "*.txn")
|
||||
);
|
||||
@@ -623,19 +650,10 @@ public class AppController implements Initializable {
|
||||
byte[] bytes = Files.readAllBytes(file.toPath());
|
||||
String name = file.getName();
|
||||
|
||||
try {
|
||||
if(Utils.isHex(bytes) || Utils.isBase64(bytes)) {
|
||||
addTransactionTab(name, file, new String(bytes, StandardCharsets.UTF_8).trim());
|
||||
} else {
|
||||
addTransactionTab(name, file, bytes);
|
||||
} catch(ParseException e) {
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
|
||||
ByteSource byteSource = new ByteSource() {
|
||||
@Override
|
||||
public InputStream openStream() {
|
||||
return inputStream;
|
||||
}
|
||||
};
|
||||
|
||||
String text = byteSource.asCharSource(Charsets.UTF_8).read().trim();
|
||||
addTransactionTab(name, file, text);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
showErrorDialog("Error opening file", e.getMessage());
|
||||
@@ -763,7 +781,8 @@ public class AppController implements Initializable {
|
||||
byte[] txBytes = transaction.bitcoinSerialize();
|
||||
UR ur = UR.fromBytes(txBytes);
|
||||
BBQR bbqr = new BBQR(BBQRType.TXN, txBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
|
||||
String raw = Utils.bytesToHex(txBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(ur, bbqr, raw, false, false, QREncoding.UR);
|
||||
qrDisplayDialog.initOwner(rootStack.getScene().getWindow());
|
||||
qrDisplayDialog.showAndWait();
|
||||
} catch(Exception e) {
|
||||
@@ -800,6 +819,7 @@ public class AppController implements Initializable {
|
||||
|
||||
String fileName = ((Label)selectedTab.getGraphic()).getText();
|
||||
if(fileName != null && !fileName.isEmpty()) {
|
||||
fileName = fileName.replace('/', '_');
|
||||
if(!fileName.endsWith(".psbt")) {
|
||||
fileName += ".psbt";
|
||||
}
|
||||
@@ -821,10 +841,10 @@ public class AppController implements Initializable {
|
||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
if(asText) {
|
||||
PrintWriter writer = new PrintWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
|
||||
writer.print(transactionTabData.getPsbt().toBase64String(includeXpubs));
|
||||
writer.print(transactionTabData.getPsbt().getForExport().toBase64String(includeXpubs));
|
||||
writer.flush();
|
||||
} else {
|
||||
outputStream.write(transactionTabData.getPsbt().serialize(includeXpubs, true));
|
||||
outputStream.write(transactionTabData.getPsbt().getForExport().serialize(includeXpubs, true));
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving PSBT", e);
|
||||
@@ -847,7 +867,7 @@ public class AppController implements Initializable {
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
String data = asBase64 ? transactionTabData.getPsbt().toBase64String() : transactionTabData.getPsbt().toString();
|
||||
String data = asBase64 ? transactionTabData.getPsbt().getForExport().toBase64String() : transactionTabData.getPsbt().getForExport().toString();
|
||||
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(data);
|
||||
@@ -861,10 +881,10 @@ public class AppController implements Initializable {
|
||||
if(tabData.getType() == TabData.TabType.TRANSACTION) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
|
||||
byte[] psbtBytes = transactionTabData.getPsbt().serialize();
|
||||
byte[] psbtBytes = transactionTabData.getPsbt().getForExport().serialize();
|
||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, false);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, QREncoding.UR);
|
||||
qrDisplayDialog.initOwner(rootStack.getScene().getWindow());
|
||||
qrDisplayDialog.show();
|
||||
}
|
||||
@@ -936,15 +956,40 @@ public class AppController implements Initializable {
|
||||
EventManager.get().post(new OpenWalletsNewWindowsStatusEvent(item.isSelected()));
|
||||
}
|
||||
|
||||
public void chunkAddresses(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setChunkAddresses(item.isSelected());
|
||||
if(item.isSelected() && !rootBox.getStyleClass().contains("chunk-addresses")) {
|
||||
rootBox.getStyleClass().add("chunk-addresses");
|
||||
} else {
|
||||
rootBox.getStyleClass().remove("chunk-addresses");
|
||||
}
|
||||
}
|
||||
|
||||
public void hideEmptyUsedAddresses(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setHideEmptyUsedAddresses(item.isSelected());
|
||||
EventManager.get().post(new HideEmptyUsedAddressesStatusEvent(item.isSelected()));
|
||||
}
|
||||
|
||||
public void hideAmounts(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setHideAmounts(item.isSelected());
|
||||
EventManager.get().post(new HideAmountsStatusEvent(item.isSelected()));
|
||||
}
|
||||
|
||||
public void useHdCameraResolution(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setHdCapture(item.isSelected());
|
||||
if(Config.get().getWebcamResolution().isStandardAspect() && item.isSelected()) {
|
||||
Config.get().setWebcamResolution(WebcamResolution.HD);
|
||||
} else if(Config.get().getWebcamResolution().isWidescreenAspect() && !item.isSelected()) {
|
||||
Config.get().setWebcamResolution(WebcamResolution.VGA);
|
||||
}
|
||||
}
|
||||
|
||||
public void mirrorCameraImage(ActionEvent event) {
|
||||
CheckMenuItem item = (CheckMenuItem)event.getSource();
|
||||
Config.get().setMirrorCapture(item.isSelected());
|
||||
}
|
||||
|
||||
public void showLoadingLog(ActionEvent event) {
|
||||
@@ -999,8 +1044,8 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
public void restart(ActionEvent event, Network network) {
|
||||
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
|
||||
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
|
||||
if(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH) == null) {
|
||||
throw new IllegalStateException("Property " + SparrowWallet.JPACKAGE_APP_PATH + " is not present");
|
||||
}
|
||||
|
||||
Args args = getRestartArgs();
|
||||
@@ -1021,9 +1066,13 @@ public class AppController implements Initializable {
|
||||
private void restart(ActionEvent event, Args args) {
|
||||
try {
|
||||
List<String> cmd = new ArrayList<>();
|
||||
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
|
||||
cmd.add(System.getProperty(SparrowWallet.JPACKAGE_APP_PATH));
|
||||
cmd.addAll(args.toParams());
|
||||
final ProcessBuilder builder = new ProcessBuilder(cmd);
|
||||
if(OsType.getCurrent() == OsType.UNIX) {
|
||||
Map<String, String> env = builder.environment();
|
||||
env.remove("LD_LIBRARY_PATH");
|
||||
}
|
||||
builder.start();
|
||||
quit(event);
|
||||
} catch(Exception e) {
|
||||
@@ -1072,7 +1121,7 @@ public class AppController implements Initializable {
|
||||
WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get();
|
||||
File walletFile = Storage.getWalletFile(nameAndBirthDate.getName());
|
||||
Storage storage = new Storage(walletFile);
|
||||
Wallet wallet = new Wallet(nameAndBirthDate.getName(), PolicyType.SINGLE, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate());
|
||||
Wallet wallet = new Wallet(nameAndBirthDate.getName(), PolicyType.SINGLE_HD, ScriptType.P2WPKH, nameAndBirthDate.getBirthDate());
|
||||
addWalletTabOrWindow(storage, wallet, false);
|
||||
}
|
||||
}
|
||||
@@ -1090,12 +1139,37 @@ public class AppController implements Initializable {
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
List<File> files = fileChooser.showOpenMultipleDialog(window);
|
||||
if(files != null) {
|
||||
configureWalletsDir(files);
|
||||
for(File file : files) {
|
||||
openWalletFile(file, forceSameWindow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void configureWalletsDir(List<File> files) {
|
||||
List<File> parentDirs = files.stream().map(File::getParentFile).distinct().collect(Collectors.toList());
|
||||
if(parentDirs.size() == 1 && !Boolean.FALSE.equals(Config.get().getSuggestChangeWalletsDir())) {
|
||||
File selectedDir = parentDirs.getFirst();
|
||||
boolean sameDir;
|
||||
try {
|
||||
sameDir = Files.isSameFile(selectedDir.toPath(), Storage.getWalletsDir().toPath());
|
||||
} catch(IOException e) {
|
||||
sameDir = selectedDir.toPath().normalize().equals(Storage.getWalletsDir().toPath().normalize());
|
||||
}
|
||||
if(!sameDir) {
|
||||
ConfirmationAlert alert = new ConfirmationAlert("Change wallets directory?",
|
||||
"Do you want to configure Sparrow to use " + selectedDir + " as the default wallets directory?", ButtonType.NO, ButtonType.YES);
|
||||
Optional<ButtonType> optType = alert.showAndWait();
|
||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||
Config.get().setWalletsDir(selectedDir);
|
||||
Config.get().setSuggestChangeWalletsDir(null);
|
||||
} else if(alert.isDontAskAgain()) {
|
||||
Config.get().setSuggestChangeWalletsDir(Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void openWalletFile(File file, boolean forceSameWindow) {
|
||||
try {
|
||||
Storage storage = new Storage(file);
|
||||
@@ -1188,28 +1262,31 @@ public class AppController implements Initializable {
|
||||
List<WalletForm> selectedWalletForms = getSelectedWalletForms();
|
||||
WalletImportDialog dlg = new WalletImportDialog(selectedWalletForms);
|
||||
dlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<Wallet> optionalWallet = dlg.showAndWait();
|
||||
if(optionalWallet.isPresent()) {
|
||||
Wallet wallet = optionalWallet.get();
|
||||
Optional<List<Wallet>> optionalWallets = dlg.showAndWait();
|
||||
if(optionalWallets.isPresent()) {
|
||||
List<Wallet> wallets = optionalWallets.get();
|
||||
|
||||
List<WalletTabData> walletTabData = getOpenWalletTabData();
|
||||
List<ExtendedKey> xpubs = wallet.getKeystores().stream().map(Keystore::getExtendedPublicKey).collect(Collectors.toList());
|
||||
Optional<WalletForm> optNewWalletForm = walletTabData.stream()
|
||||
.map(WalletTabData::getWalletForm)
|
||||
.filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI &&
|
||||
wf.getSettingsWalletForm().getWallet().getScriptType() == wallet.getScriptType() && !wf.getSettingsWalletForm().getWallet().isValid() &&
|
||||
wf.getSettingsWalletForm().getWallet().getKeystores().stream().map(Keystore::getExtendedPublicKey).anyMatch(xpubs::contains)).findFirst();
|
||||
if(optNewWalletForm.isPresent()) {
|
||||
EventManager.get().post(new ExistingWalletImportedEvent(optNewWalletForm.get().getWalletId(), wallet));
|
||||
selectTab(optNewWalletForm.get().getWallet());
|
||||
} else if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) {
|
||||
addImportedWallet(wallet);
|
||||
for(Wallet wallet : wallets) {
|
||||
List<WalletTabData> walletTabData = getOpenWalletTabData();
|
||||
List<ExtendedKey> xpubs = wallet.getKeystores().stream().map(Keystore::getExtendedPublicKey).collect(Collectors.toList());
|
||||
Optional<WalletForm> optNewWalletForm = walletTabData.stream()
|
||||
.map(WalletTabData::getWalletForm)
|
||||
.filter(wf -> wf.getSettingsWalletForm() != null && wf.getSettingsWalletForm().getWallet().getPolicyType() == PolicyType.MULTI_HD &&
|
||||
wf.getSettingsWalletForm().getWallet().getScriptType() == wallet.getScriptType() && !wf.getSettingsWalletForm().getWallet().isValid() &&
|
||||
wf.getSettingsWalletForm().getWallet().getKeystores().stream().map(Keystore::getExtendedPublicKey).anyMatch(xpubs::contains)).findFirst();
|
||||
if(optNewWalletForm.isPresent()) {
|
||||
EventManager.get().post(new ExistingWalletImportedEvent(optNewWalletForm.get().getWalletId(), wallet));
|
||||
selectTab(optNewWalletForm.get().getWallet());
|
||||
} else if(selectedWalletForms.isEmpty() || wallet != selectedWalletForms.get(0).getWallet()) {
|
||||
addImportedWallet(wallet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean attemptImportWallet(File file, SecureString password) {
|
||||
List<WalletImport> walletImporters = List.of(new ColdcardSinglesig(), new ColdcardMultisig(),
|
||||
new Bip129(),
|
||||
new Electrum(),
|
||||
new SpecterDesktop(),
|
||||
new Descriptor(),
|
||||
@@ -1247,13 +1324,20 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void addImportedWallet(Wallet wallet) {
|
||||
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
|
||||
if(AppServices.disallowAnyInvalidDerivationPaths(wallet)) {
|
||||
return;
|
||||
}
|
||||
|
||||
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getPolicyType(), wallet.getBirthDate(), false);
|
||||
nameDlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
|
||||
if(optNameAndBirthDate.isPresent()) {
|
||||
WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get();
|
||||
wallet.setName(nameAndBirthDate.getName());
|
||||
wallet.setBirthDate(nameAndBirthDate.getBirthDate());
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP && wallet.getBirthDate() == null) {
|
||||
wallet.setBirthDate(new Date());
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@@ -1362,7 +1446,7 @@ public class AppController implements Initializable {
|
||||
public void exportWallet(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm);
|
||||
WalletExportDialog dlg = new WalletExportDialog(selectedWalletForm, getSelectedWalletForms());
|
||||
dlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<Wallet> wallet = dlg.showAndWait();
|
||||
if(wallet.isPresent()) {
|
||||
@@ -1371,18 +1455,18 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void openPreferences(ActionEvent event) {
|
||||
openPreferences(PreferenceGroup.GENERAL);
|
||||
public void openSettings(ActionEvent event) {
|
||||
openSettings(SettingsGroup.GENERAL);
|
||||
}
|
||||
|
||||
public void openServerPreferences(ActionEvent event) {
|
||||
openPreferences(PreferenceGroup.SERVER);
|
||||
public void openServerSettings(ActionEvent event) {
|
||||
openSettings(SettingsGroup.SERVER);
|
||||
}
|
||||
|
||||
private void openPreferences(PreferenceGroup preferenceGroup) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(preferenceGroup);
|
||||
preferencesDialog.initOwner(rootStack.getScene().getWindow());
|
||||
preferencesDialog.showAndWait();
|
||||
private void openSettings(SettingsGroup settingsGroup) {
|
||||
SettingsDialog settingsDialog = new SettingsDialog(settingsGroup);
|
||||
settingsDialog.initOwner(rootStack.getScene().getWindow());
|
||||
settingsDialog.showAndWait();
|
||||
configureSwitchServer();
|
||||
serverToggle.setDisable(!Config.get().hasServer());
|
||||
}
|
||||
@@ -1392,7 +1476,7 @@ public class AppController implements Initializable {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
if(wallet.getKeystores().size() == 1) {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_HD || wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
//Can sign and verify
|
||||
messageSignDialog = new MessageSignDialog(wallet);
|
||||
}
|
||||
@@ -1408,6 +1492,10 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
public void sendToMany(ActionEvent event) {
|
||||
sendToMany(Collections.emptyList());
|
||||
}
|
||||
|
||||
private void sendToMany(List<Payment> initialPayments) {
|
||||
if(sendToManyDialog != null) {
|
||||
Stage stage = (Stage)sendToManyDialog.getDialogPane().getScene().getWindow();
|
||||
stage.setAlwaysOnTop(true);
|
||||
@@ -1423,7 +1511,7 @@ public class AppController implements Initializable {
|
||||
bitcoinUnit = wallet.getAutoUnit();
|
||||
}
|
||||
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit);
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
|
||||
sendToManyDialog.initModality(Modality.NONE);
|
||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||
sendToManyDialog = null;
|
||||
@@ -1464,6 +1552,7 @@ public class AppController implements Initializable {
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.setAlwaysOnTop(false);
|
||||
if(event.getSource() instanceof File file) {
|
||||
downloadVerifierDialog.setInitialFile(file);
|
||||
downloadVerifierDialog.setSignatureFile(file);
|
||||
}
|
||||
return;
|
||||
@@ -1874,6 +1963,58 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void addTransactionTab(String name, File file, PSBT psbt) {
|
||||
//Add any missing previous outputs if available in open wallets
|
||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||
if(psbtInput.getUtxo() == null) {
|
||||
for(Wallet wallet : AppServices.get().getOpenWallets().keySet().stream().filter(Wallet::isValid).toList()) {
|
||||
TransactionOutPoint outpoint = psbtInput.getInput().getOutpoint();
|
||||
BlockTransaction blockTransaction = wallet.getWalletTransaction(outpoint.getHash());
|
||||
if(blockTransaction != null && blockTransaction.getTransaction().getOutputs().size() > outpoint.getIndex()) {
|
||||
psbtInput.setNonWitnessUtxo(blockTransaction.getTransaction());
|
||||
ScriptType type = psbtInput.getScriptType();
|
||||
if(type != null && Arrays.asList(ScriptType.WITNESS_TYPES).contains(type)) {
|
||||
psbtInput.setWitnessUtxo(blockTransaction.getTransaction().getOutputs().get((int)outpoint.getIndex()));
|
||||
psbtInput.setNonWitnessUtxo(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Add DNS payment information if not already cached
|
||||
for(PSBTOutput psbtOutput : psbt.getPsbtOutputs()) {
|
||||
if(psbtOutput.getDnssecProof() != null && !psbtOutput.getDnssecProof().isEmpty()) {
|
||||
Address address = psbtOutput.getScript() != null ? psbtOutput.getScript().getToAddress() : null;
|
||||
if(address != null && DnsPaymentCache.getDnsPayment(address) == null) {
|
||||
try {
|
||||
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
|
||||
if(optDnsPayment.isPresent() && address.equals(optDnsPayment.get().bitcoinURI().getAddress())) {
|
||||
DnsPaymentCache.putDnsPayment(address, optDnsPayment.get());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error resolving DNS payment", e);
|
||||
}
|
||||
}
|
||||
|
||||
SilentPaymentAddress silentPaymentAddress = psbtOutput.getSilentPaymentAddress();
|
||||
if(address != null && silentPaymentAddress == null) {
|
||||
silentPaymentAddress = AppServices.get().getOpenWallets().keySet().stream()
|
||||
.map(wallet -> wallet.getSilentPaymentAddress(address)).filter(Objects::nonNull).findFirst().orElse(null);
|
||||
}
|
||||
if(silentPaymentAddress != null && DnsPaymentCache.getDnsPayment(silentPaymentAddress) == null) {
|
||||
try {
|
||||
Optional<DnsPayment> optDnsPayment = psbtOutput.getDnsPayment();
|
||||
if(optDnsPayment.isPresent() && silentPaymentAddress.equals(optDnsPayment.get().bitcoinURI().getSilentPaymentAddress())) {
|
||||
DnsPaymentCache.putDnsPayment(silentPaymentAddress, optDnsPayment.get());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error resolving DNS payment", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
|
||||
if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) {
|
||||
EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt));
|
||||
@@ -1891,45 +2032,19 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void addTransactionTab(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||
addTransactionTab(null, null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex);
|
||||
addTransactionTab(blockTransaction.getLabel(), null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex);
|
||||
}
|
||||
|
||||
private void addTransactionTab(String name, File file, Transaction transaction, PSBT psbt, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
TabData tabData = (TabData)tab.getUserData();
|
||||
if(tabData instanceof TransactionTabData) {
|
||||
TransactionTabData transactionTabData = (TransactionTabData)tabData;
|
||||
if(tabData instanceof TransactionTabData transactionTabData) {
|
||||
if(isExistingTransaction(transactionTabData, transaction, psbt, getTabName(tab))) {
|
||||
handleTransactionMerge(transactionTabData, psbt, name, tab);
|
||||
return;
|
||||
}
|
||||
|
||||
//If an exact match bytewise of an existing tab, return that tab
|
||||
if(Arrays.equals(transactionTabData.getTransaction().bitcoinSerialize(), transaction.bitcoinSerialize())) {
|
||||
if(transactionTabData.getPsbt() != null && psbt != null && !transactionTabData.getPsbt().isFinalized()) {
|
||||
if(!psbt.isFinalized()) {
|
||||
//As per BIP174, combine PSBTs with matching transactions so long as they are not yet finalized
|
||||
transactionTabData.getPsbt().combine(psbt);
|
||||
if(name != null && !name.isEmpty()) {
|
||||
((Label)tab.getGraphic()).setText(name);
|
||||
}
|
||||
|
||||
EventManager.get().post(new PSBTCombinedEvent(transactionTabData.getPsbt()));
|
||||
} else {
|
||||
//If the new PSBT is finalized, copy the finalized fields to the existing unfinalized PSBT
|
||||
for(int i = 0; i < transactionTabData.getPsbt().getPsbtInputs().size(); i++) {
|
||||
PSBTInput existingInput = transactionTabData.getPsbt().getPsbtInputs().get(i);
|
||||
PSBTInput finalizedInput = psbt.getPsbtInputs().get(i);
|
||||
existingInput.setFinalScriptSig(finalizedInput.getFinalScriptSig());
|
||||
existingInput.setFinalScriptWitness(finalizedInput.getFinalScriptWitness());
|
||||
existingInput.clearNonFinalFields();
|
||||
}
|
||||
|
||||
if(name != null && !name.isEmpty()) {
|
||||
((Label)tab.getGraphic()).setText(name);
|
||||
}
|
||||
|
||||
EventManager.get().post(new PSBTFinalizedEvent(transactionTabData.getPsbt()));
|
||||
}
|
||||
}
|
||||
|
||||
tabs.getSelectionModel().select(tab);
|
||||
if(transactionTabData.getPsbt() != null && transactionTabData.getPsbt().possibleUnverifiableSilentPaymentsTransaction(transaction) && !openUnverifiableTransaction(getTabName(tab))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1957,8 +2072,13 @@ public class AppController implements Initializable {
|
||||
glyph.setFontSize(10.0);
|
||||
glyph.setOpacity(TAB_LABEL_GRAPHIC_OPACITY_ACTIVE);
|
||||
Label tabLabel = new Label(tabName);
|
||||
tabLabel.setMaxWidth(TAB_LABEL_MAX_WIDTH);
|
||||
tabLabel.setGraphic(glyph);
|
||||
tabLabel.setGraphicTextGap(5.0);
|
||||
if(TextUtils.computeTextWidth(tabLabel.getFont(), tabName, 0.0D) > TAB_LABEL_MAX_WIDTH) {
|
||||
Tooltip tooltip = new Tooltip(tabName);
|
||||
tabLabel.setTooltip(tooltip);
|
||||
}
|
||||
tab.setGraphic(tabLabel);
|
||||
tab.setContextMenu(getTabContextMenu(tab));
|
||||
tab.setClosable(true);
|
||||
@@ -1991,6 +2111,69 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isExistingTransaction(TransactionTabData transactionTabData, Transaction transaction, PSBT psbt, String tabName) {
|
||||
PSBT currentPsbt = transactionTabData.getPsbt();
|
||||
Transaction currentTransaction = transactionTabData.getTransaction();
|
||||
|
||||
if(currentPsbt != null && psbt != null && currentPsbt.matches(psbt)) {
|
||||
return true;
|
||||
} else if(currentTransaction.getTxId().equals(transaction.getTxId())) {
|
||||
if(currentTransaction.getWTxId().equals(transaction.getWTxId())) {
|
||||
return true;
|
||||
} else if(currentPsbt == null) {
|
||||
AppServices.showWarningDialog("Suspicious Transaction",
|
||||
"This transaction has the same txid as the transaction in tab " + tabName + ", but contains different witnesses. It will be opened in a separate tab.");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleTransactionMerge(TransactionTabData transactionTabData, PSBT psbt, String name, Tab tab) {
|
||||
PSBT currentPsbt = transactionTabData.getPsbt();
|
||||
|
||||
if(currentPsbt != null && psbt != null && !currentPsbt.isFinalized()) {
|
||||
if(!psbt.isFinalized()) {
|
||||
//As per BIP174, combine PSBTs with matching transactions so long as they are not yet finalized
|
||||
try {
|
||||
currentPsbt.verifyCombinedSignatures(psbt);
|
||||
currentPsbt.combine(psbt);
|
||||
setTabName(tab, name);
|
||||
EventManager.get().post(new PSBTCombinedEvent(currentPsbt));
|
||||
} catch(PSBTSignatureException e) {
|
||||
AppServices.showErrorDialog("Invalid PSBT", e.getMessage());
|
||||
}
|
||||
} else {
|
||||
//If the new PSBT is finalized, copy the finalized fields to the existing unfinalized PSBT
|
||||
currentPsbt.copyFinalizedFields(psbt);
|
||||
setTabName(tab, name);
|
||||
EventManager.get().post(new PSBTFinalizedEvent(currentPsbt));
|
||||
}
|
||||
}
|
||||
|
||||
tabs.getSelectionModel().select(tab);
|
||||
}
|
||||
|
||||
private boolean openUnverifiableTransaction(String tabName) {
|
||||
Optional<ButtonType> result = AppServices.showWarningDialog(
|
||||
"Unverifiable Silent Payments Transaction",
|
||||
"This transaction contains an unverifiable silent payments output.\n\n" +
|
||||
"The tab " + tabName + " contains a similar transaction spending to a silent payments address, " +
|
||||
"but this transaction does not contain enough information to determine if the recipient address is correct.\n\n" +
|
||||
"Open the transaction in another tab?", ButtonType.YES, ButtonType.NO);
|
||||
return result.isPresent() && result.get() == ButtonType.YES;
|
||||
}
|
||||
|
||||
private String getTabName(Tab tab) {
|
||||
return ((Label)tab.getGraphic()).getText();
|
||||
}
|
||||
|
||||
private void setTabName(Tab tab, String name){
|
||||
if(name != null && !name.isEmpty()) {
|
||||
((Label)tab.getGraphic()).setText(name);
|
||||
}
|
||||
}
|
||||
|
||||
private ContextMenu getTabContextMenu(Tab tab) {
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
|
||||
@@ -2007,23 +2190,33 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
MenuItem moveRight = new MenuItem("Move Right");
|
||||
moveRight.setAccelerator(new KeyCodeCombination(KeyCode.RIGHT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
|
||||
moveRight.setOnAction(event -> {
|
||||
int index = tabs.getTabs().indexOf(tab);
|
||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||
if(currentIndex + 1 >= tabs.getTabs().size()) {
|
||||
return;
|
||||
}
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
tabs.getTabs().removeListener(tabsChangeListener);
|
||||
tabs.getTabs().remove(tab);
|
||||
tabs.getTabs().add(index + 1, tab);
|
||||
tabs.getTabs().remove(selectedTab);
|
||||
tabs.getTabs().add(currentIndex + 1, selectedTab);
|
||||
tabs.getTabs().addListener(tabsChangeListener);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
tabs.getSelectionModel().select(selectedTab);
|
||||
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
||||
});
|
||||
MenuItem moveLeft = new MenuItem("Move Left");
|
||||
moveLeft.setAccelerator(new KeyCodeCombination(KeyCode.LEFT, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN));
|
||||
moveLeft.setOnAction(event -> {
|
||||
int index = tabs.getTabs().indexOf(tab);
|
||||
int currentIndex = tabs.getSelectionModel().getSelectedIndex();
|
||||
if(currentIndex == 0) {
|
||||
return;
|
||||
}
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
tabs.getTabs().removeListener(tabsChangeListener);
|
||||
tabs.getTabs().remove(tab);
|
||||
tabs.getTabs().add(index - 1, tab);
|
||||
tabs.getTabs().remove(selectedTab);
|
||||
tabs.getTabs().add(currentIndex - 1, selectedTab);
|
||||
tabs.getTabs().addListener(tabsChangeListener);
|
||||
tabs.getSelectionModel().select(tab);
|
||||
tabs.getSelectionModel().select(selectedTab);
|
||||
EventManager.get().post(new RequestOpenWalletsEvent()); //Rearrange recent files list
|
||||
});
|
||||
contextMenu.getItems().addAll(moveRight, moveLeft);
|
||||
@@ -2080,7 +2273,7 @@ public class AppController implements Initializable {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Image");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("Images", "*.png", "*.jpg", "*.jpeg", "*.gif")
|
||||
);
|
||||
|
||||
@@ -2456,7 +2649,7 @@ public class AppController implements Initializable {
|
||||
if(event instanceof TransactionTabSelectedEvent) {
|
||||
TransactionTabSelectedEvent txTabEvent = (TransactionTabSelectedEvent)event;
|
||||
TransactionTabData transactionTabData = txTabEvent.getTransactionTabData();
|
||||
if(transactionTabData.getPsbt() == null || transactionTabData.getPsbt().getTransaction() != transactionTabData.getTransaction()) {
|
||||
if(transactionTabData.getPsbt() == null || !transactionTabData.getPsbt().getTransaction().getTxId().equals(transactionTabData.getTransaction().getTxId())) {
|
||||
saveTransaction.setVisible(true);
|
||||
saveTransaction.setDisable(false);
|
||||
} else {
|
||||
@@ -2595,7 +2788,6 @@ public class AppController implements Initializable {
|
||||
}
|
||||
});
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
String walletName = event.getWallet().getFullDisplayName();
|
||||
if(walletName.length() > 40) {
|
||||
walletName = walletName.substring(0, 40) + "...";
|
||||
@@ -2604,10 +2796,10 @@ public class AppController implements Initializable {
|
||||
Notifications notificationBuilder = Notifications.create()
|
||||
.title("Sparrow - " + walletName)
|
||||
.text(text)
|
||||
.graphic(new ImageView(image))
|
||||
.graphic(new DialogImage(DialogImage.Type.SPARROW))
|
||||
.hideAfter(Duration.seconds(15))
|
||||
.position(Pos.TOP_RIGHT)
|
||||
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new ImageView(image)))
|
||||
.threshold(5, Notifications.create().title("Sparrow").text("Multiple new wallet transactions").graphic(new DialogImage(DialogImage.Type.SPARROW)))
|
||||
.onAction(e -> selectTab(event.getWallet()));
|
||||
|
||||
//If controlsfx can't find our window, we must set the window ourselves (unfortunately notification is then shown within this window)
|
||||
@@ -2723,8 +2915,8 @@ public class AppController implements Initializable {
|
||||
@Subscribe
|
||||
public void connectionFailed(ConnectionFailedEvent event) {
|
||||
String status = CONNECTION_FAILED_PREFIX + event.getMessage();
|
||||
Hyperlink hyperlink = new Hyperlink("Server Preferences");
|
||||
hyperlink.setOnAction(this::openServerPreferences);
|
||||
Hyperlink hyperlink = new Hyperlink("Server Settings");
|
||||
hyperlink.setOnAction(this::openServerSettings);
|
||||
statusUpdated(new StatusEvent(status, hyperlink));
|
||||
serverToggleStopAnimation();
|
||||
setTorIcon();
|
||||
@@ -2848,6 +3040,7 @@ public class AppController implements Initializable {
|
||||
}
|
||||
} else if(event.isCompleted()) {
|
||||
serverToggle.setDisable(false);
|
||||
statusBar.setProgress(0);
|
||||
if(statusBar.getText().startsWith("Scanning...")) {
|
||||
statusBar.setText("");
|
||||
}
|
||||
@@ -3025,6 +3218,11 @@ public class AppController implements Initializable {
|
||||
hideEmptyUsedAddresses.setSelected(event.isHideEmptyUsedAddresses());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void hideAmountsStatusChanged(HideAmountsStatusEvent event) {
|
||||
hideAmounts.setSelected(event.isHideAmounts());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void requestOpenWallets(RequestOpenWalletsEvent event) {
|
||||
EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWalletTabData()));
|
||||
@@ -3034,6 +3232,8 @@ public class AppController implements Initializable {
|
||||
public void requestWalletOpen(RequestWalletOpenEvent event) {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
if(event.getFile() != null) {
|
||||
Optional<Tab> optExisting = tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData walletTabData && walletTabData.getStorage().getWalletFile().equals(event.getFile())).findFirst();
|
||||
optExisting.ifPresent(tab -> tabs.getTabs().remove(tab));
|
||||
openWalletFile(event.getFile(), true);
|
||||
} else {
|
||||
openWallet(true);
|
||||
@@ -3066,6 +3266,11 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void requestSendToMany(RequestSendToManyEvent event) {
|
||||
sendToMany(event.getPayments());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void functionAction(FunctionActionEvent event) {
|
||||
selectTab(event.getWallet());
|
||||
@@ -3116,4 +3321,14 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void webcamResolutionChanged(WebcamResolutionChangedEvent event) {
|
||||
useHdCameraResolutionProperty.set(event.getResolution().isWidescreenAspect());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void webcamMirroredChanged(WebcamMirroredChangedEvent event) {
|
||||
mirrorCameraImageProperty.set(event.isMirrored());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
@@ -12,6 +13,7 @@ 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.DialogImage;
|
||||
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.net.Auth47;
|
||||
@@ -24,6 +26,8 @@ import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@@ -41,7 +45,6 @@ import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Dialog;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Screen;
|
||||
@@ -65,8 +68,12 @@ import java.time.ZonedDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppController.CONNECTION_FAILED_PREFIX;
|
||||
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
|
||||
|
||||
public class AppServices {
|
||||
@@ -86,8 +93,7 @@ public class AppServices {
|
||||
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> LONG_FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L, 2048L, 4096L, 8192L);
|
||||
public static final List<Long> FEE_RATES_RANGE = LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
|
||||
private static final List<Double> LONG_FEE_RATES_RANGE = List.of(1d, 2d, 4d, 8d, 16d, 32d, 64d, 128d, 256d, 512d, 1024d, 2048d, 4096d, 8192d);
|
||||
public static final double FALLBACK_FEE_RATE = 20000d / 1000;
|
||||
public static final double TESTNET_FALLBACK_FEE_RATE = 1000d / 1000;
|
||||
|
||||
@@ -103,6 +109,8 @@ public class AppServices {
|
||||
|
||||
private TrayManager trayManager;
|
||||
|
||||
private final PublishSubject<NewBlockEvent> newBlockSubject = PublishSubject.create();
|
||||
|
||||
private static Image windowIcon;
|
||||
|
||||
private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false);
|
||||
@@ -125,12 +133,18 @@ public class AppServices {
|
||||
|
||||
private static BlockHeader latestBlockHeader;
|
||||
|
||||
private static final Map<Integer, BlockSummary> blockSummaries = new ConcurrentHashMap<>();
|
||||
|
||||
private static Map<Integer, Double> targetBlockFeeRates;
|
||||
|
||||
private static Double nextBlockMedianFeeRate;
|
||||
|
||||
private static final TreeMap<Date, Set<MempoolRateSize>> mempoolHistogram = new TreeMap<>();
|
||||
|
||||
private static Double minimumRelayFeeRate;
|
||||
|
||||
private static Double serverMinimumRelayFeeRate;
|
||||
|
||||
private static CurrencyRate fiatCurrencyExchangeRate;
|
||||
|
||||
private static List<Device> devices;
|
||||
@@ -181,6 +195,12 @@ public class AppServices {
|
||||
private AppServices(Application application, InteractionServices interactionServices) {
|
||||
this.application = application;
|
||||
this.interactionServices = interactionServices;
|
||||
|
||||
newBlockSubject.buffer(4, TimeUnit.SECONDS)
|
||||
.filter(newBlockEvents -> !newBlockEvents.isEmpty())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(this::fetchBlockSummaries, exception -> log.error("Error fetching block summaries", exception));
|
||||
|
||||
EventManager.get().register(this);
|
||||
}
|
||||
|
||||
@@ -194,6 +214,7 @@ public class AppServices {
|
||||
preventSleepService = createPreventSleepService();
|
||||
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
minimumRelayFeeRate = getConfiguredMinimumRelayFeeRate(config);
|
||||
|
||||
if(config.getMode() == Mode.ONLINE) {
|
||||
if(config.requiresInternalTor()) {
|
||||
@@ -260,7 +281,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
if(Tor.getDefault() != null) {
|
||||
Tor.getDefault().getTorManager().destroy(true, success -> {});
|
||||
Tor.getDefault().close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,12 +311,6 @@ public class AppServices {
|
||||
if(event != null) {
|
||||
EventManager.get().post(event);
|
||||
}
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
if(event instanceof ConnectionEvent && Network.get().equals(Network.MAINNET) && feeRatesSource.isExternal()) {
|
||||
EventManager.get().post(new FeeRatesSourceChangedEvent(feeRatesSource));
|
||||
}
|
||||
});
|
||||
connectionService.setOnFailed(failEvent -> {
|
||||
//Close connection here to create a new transport next time we try
|
||||
@@ -315,6 +330,9 @@ public class AppServices {
|
||||
"\n\nChange the configured server certificate if you would like to proceed.");
|
||||
} else {
|
||||
crtFile = Storage.getCertificateFile(tlsServerException.getServer().getHost());
|
||||
if(crtFile == null) {
|
||||
crtFile = Storage.getCaCertificateFile(tlsServerException.getServer().getHost());
|
||||
}
|
||||
if(crtFile != null) {
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
|
||||
"\n\nThis may be simply due to a certificate renewal, or it may indicate a man-in-the-middle attack." +
|
||||
@@ -351,15 +369,18 @@ public class AppServices {
|
||||
onlineProperty.setValue(false);
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
|
||||
log.debug("Connection failed", failEvent.getSource().getException());
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
|
||||
Config.get().changePublicServer();
|
||||
connectionService.setPeriod(Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS));
|
||||
boolean changed = changePublicServer();
|
||||
connectionService.setPeriod(changed ? Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS) : Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
|
||||
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
||||
if(!changed) {
|
||||
Platform.runLater(() -> EventManager.get().post(new StatusEvent(CONNECTION_FAILED_PREFIX + "No public servers available that can serve the open wallets, retrying later...")));
|
||||
}
|
||||
} else {
|
||||
connectionService.setPeriod(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
|
||||
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
||||
}
|
||||
|
||||
log.debug("Connection failed", failEvent.getSource().getException());
|
||||
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
||||
});
|
||||
|
||||
return connectionService;
|
||||
@@ -379,7 +400,7 @@ public class AppServices {
|
||||
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
|
||||
currency == null ? DEFAULT_FIAT_CURRENCY : currency);
|
||||
//Delay startup on first run, Windows requires a longer delay
|
||||
ratesService.setDelay(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS ? Duration.seconds(RATES_DELAY_SECS_WINDOWS) : Duration.seconds(RATES_DELAY_SECS_DEFAULT));
|
||||
ratesService.setDelay(OsType.getCurrent() == OsType.WINDOWS ? Duration.seconds(RATES_DELAY_SECS_WINDOWS) : Duration.seconds(RATES_DELAY_SECS_DEFAULT));
|
||||
ratesService.setPeriod(Duration.seconds(RATES_PERIOD_SECS));
|
||||
ratesService.setRestartOnFailure(true);
|
||||
|
||||
@@ -479,6 +500,26 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchFeeRates() {
|
||||
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
|
||||
feeRatesService = createFeeRatesService();
|
||||
feeRatesService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchBlockSummaries(List<NewBlockEvent> newBlockEvents) {
|
||||
if(isConnected()) {
|
||||
ElectrumServer.BlockSummaryService blockSummaryService = new ElectrumServer.BlockSummaryService(newBlockEvents);
|
||||
blockSummaryService.setOnSucceeded(_ -> {
|
||||
EventManager.get().post(blockSummaryService.getValue());
|
||||
});
|
||||
blockSummaryService.setOnFailed(failedState -> {
|
||||
log.error("Error fetching block summaries", failedState.getSource().getException());
|
||||
});
|
||||
blockSummaryService.start();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isTorRunning() {
|
||||
return Tor.getDefault() != null;
|
||||
}
|
||||
@@ -573,6 +614,34 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
public static void runAfterDelay(long delay, Runnable runnable) {
|
||||
if(delay <= 0) {
|
||||
if(Platform.isFxApplicationThread()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
Platform.runLater(runnable);
|
||||
}
|
||||
} else {
|
||||
ScheduledService<Void> delayService = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
delayService.setOnSucceeded(_ -> {
|
||||
delayService.cancel();
|
||||
runnable.run();
|
||||
});
|
||||
delayService.setDelay(Duration.millis(delay));
|
||||
delayService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private static Image getWindowIcon() {
|
||||
if(windowIcon == null) {
|
||||
windowIcon = new Image(SparrowWallet.class.getResourceAsStream("/image/sparrow-icon.png"));
|
||||
@@ -591,7 +660,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
private static double getReducedWindowHeight() {
|
||||
return org.controlsfx.tools.Platform.getCurrent() != org.controlsfx.tools.Platform.OSX ? 802d : 768d; //Check for menu bar of ~34px
|
||||
return OsType.getCurrent() != OsType.MACOS ? 802d : 768d; //Check for menu bar of ~34px
|
||||
}
|
||||
|
||||
public Application getApplication() {
|
||||
@@ -676,6 +745,10 @@ public class AppServices {
|
||||
return latestBlockHeader;
|
||||
}
|
||||
|
||||
public static Map<Integer, BlockSummary> getBlockSummaries() {
|
||||
return blockSummaries;
|
||||
}
|
||||
|
||||
public static Double getDefaultFeeRate() {
|
||||
int defaultTarget = TARGET_BLOCKS_RANGE.get((TARGET_BLOCKS_RANGE.size() / 2) - 1);
|
||||
return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget);
|
||||
@@ -687,6 +760,30 @@ public class AppServices {
|
||||
return Math.max(minRate, Transaction.DUST_RELAY_TX_FEE);
|
||||
}
|
||||
|
||||
public static List<Double> getLongFeeRatesRange() {
|
||||
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
return LONG_FEE_RATES_RANGE;
|
||||
} else {
|
||||
List<Double> longFeeRatesRange = new ArrayList<>();
|
||||
longFeeRatesRange.add(minimumRelayFeeRate);
|
||||
longFeeRatesRange.addAll(LONG_FEE_RATES_RANGE);
|
||||
return longFeeRatesRange;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Double> getFeeRatesRange() {
|
||||
if(minimumRelayFeeRate == null || minimumRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
return LONG_FEE_RATES_RANGE.subList(0, LONG_FEE_RATES_RANGE.size() - 3);
|
||||
} else {
|
||||
List<Double> longFeeRatesRange = getLongFeeRatesRange();
|
||||
return longFeeRatesRange.subList(0, longFeeRatesRange.size() - 4);
|
||||
}
|
||||
}
|
||||
|
||||
public static Double getNextBlockMedianFeeRate() {
|
||||
return nextBlockMedianFeeRate == null ? getDefaultFeeRate() : nextBlockMedianFeeRate;
|
||||
}
|
||||
|
||||
public static double getFallbackFeeRate() {
|
||||
return Network.get() == Network.MAINNET ? FALLBACK_FEE_RATE : TESTNET_FALLBACK_FEE_RATE;
|
||||
}
|
||||
@@ -721,10 +818,18 @@ public class AppServices {
|
||||
});
|
||||
}
|
||||
|
||||
public static Double getConfiguredMinimumRelayFeeRate(Config config) {
|
||||
return config.getMinRelayFeeRate() >= 0d && config.getMinRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE ? config.getMinRelayFeeRate() : null;
|
||||
}
|
||||
|
||||
public static Double getMinimumRelayFeeRate() {
|
||||
return minimumRelayFeeRate == null ? Transaction.DEFAULT_MIN_RELAY_FEE : minimumRelayFeeRate;
|
||||
}
|
||||
|
||||
public static Double getServerMinimumRelayFeeRate() {
|
||||
return serverMinimumRelayFeeRate;
|
||||
}
|
||||
|
||||
public static CurrencyRate getFiatCurrencyExchangeRate() {
|
||||
return fiatCurrencyExchangeRate;
|
||||
}
|
||||
@@ -738,8 +843,8 @@ public class AppServices {
|
||||
}
|
||||
|
||||
public static void addPayjoinURI(BitcoinURI bitcoinURI) {
|
||||
if(bitcoinURI.getPayjoinUrl() == null) {
|
||||
throw new IllegalArgumentException("Not a payjoin URI");
|
||||
if(bitcoinURI.getPayjoinUrl() == null || bitcoinURI.getAddress() == null) {
|
||||
throw new IllegalArgumentException("Not a valid payjoin URI");
|
||||
}
|
||||
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
|
||||
}
|
||||
@@ -751,6 +856,10 @@ public class AppServices {
|
||||
public static void clearTransactionHistoryCache(Wallet wallet) {
|
||||
ElectrumServer.clearRetrievedScriptHashes(wallet);
|
||||
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP && wallet.isValid()) {
|
||||
ElectrumServer.releaseSilentPaymentSubscription(wallet.getSilentPaymentScanAddress());
|
||||
}
|
||||
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(childWallet.isNested()) {
|
||||
AppServices.clearTransactionHistoryCache(childWallet);
|
||||
@@ -762,6 +871,22 @@ public class AppServices {
|
||||
return Storage.isWalletFile(file);
|
||||
}
|
||||
|
||||
public boolean changePublicServer() {
|
||||
List<PolicyType> policyTypes = getOpenWallets().keySet().stream().map(Wallet::getPolicyType).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
return changePublicServer(policyTypes.isEmpty() ? List.of(PolicyType.SINGLE_HD) : policyTypes);
|
||||
}
|
||||
|
||||
private boolean changePublicServer(List<PolicyType> policyTypes) {
|
||||
Config config = Config.get();
|
||||
List<Server> otherServers = PublicElectrumServer.getServers().stream().filter(pes -> pes.supportsAllPolicyTypes(policyTypes))
|
||||
.map(PublicElectrumServer::getServer).filter(server -> !server.equals(config.getPublicElectrumServer())).collect(Collectors.toList());
|
||||
if(!otherServers.isEmpty()) {
|
||||
config.setPublicElectrumServer(otherServers.get(ThreadLocalRandom.current().nextInt(otherServers.size())));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
|
||||
return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons);
|
||||
}
|
||||
@@ -790,8 +915,13 @@ public class AppServices {
|
||||
Stage stage = (Stage)window;
|
||||
stage.getIcons().add(getWindowIcon());
|
||||
|
||||
if(stage.getScene() != null && Config.get().getTheme() == Theme.DARK) {
|
||||
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||
if(stage.getScene() != null) {
|
||||
if(Config.get().getTheme() == Theme.DARK) {
|
||||
stage.getScene().getStylesheets().add(AppServices.class.getResource("darktheme.css").toExternalForm());
|
||||
}
|
||||
if(Config.get().isChunkAddresses()) {
|
||||
stage.getScene().getRoot().getStyleClass().add("chunk-addresses");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -972,7 +1102,7 @@ public class AppServices {
|
||||
try {
|
||||
Auth47 auth47 = new Auth47(uri);
|
||||
List<ScriptType> scriptTypes = PaymentCode.SEGWIT_SCRIPT_TYPES;
|
||||
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
|
||||
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE_HD), scriptTypes, false, true, "login to " + auth47.getCallback().getHost(), true);
|
||||
|
||||
if(wallet != null) {
|
||||
try {
|
||||
@@ -992,8 +1122,8 @@ public class AppServices {
|
||||
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);
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD);
|
||||
Wallet wallet = selectWallet(List.of(PolicyType.SINGLE_HD), scriptTypes, true, true, lnurlAuth.getLoginMessage(), true);
|
||||
|
||||
if(wallet != null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
@@ -1066,8 +1196,7 @@ public class AppServices {
|
||||
walletChoiceDialog.initOwner(getActiveWindow());
|
||||
walletChoiceDialog.setTitle("Choose Wallet");
|
||||
walletChoiceDialog.setHeaderText("Choose a wallet to " + actionDescription);
|
||||
Image image = new Image("/image/sparrow-small.png");
|
||||
walletChoiceDialog.getDialogPane().setGraphic(new ImageView(image));
|
||||
walletChoiceDialog.getDialogPane().setGraphic(new DialogImage(DialogImage.Type.SPARROW));
|
||||
setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow());
|
||||
moveToActiveWindowScreen(walletChoiceDialog);
|
||||
Optional<Wallet> optWallet = walletChoiceDialog.showAndWait();
|
||||
@@ -1079,10 +1208,36 @@ public class AppServices {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public static boolean disallowAnyInvalidDerivationPaths(Wallet wallet) {
|
||||
Optional<ScriptType> optInvalidScriptType = wallet.getKeystores().stream()
|
||||
.filter(keystore -> keystore.getKeyDerivation() != null)
|
||||
.map(keystore -> wallet.getOtherScriptTypeMatchingDerivation(keystore.getKeyDerivation().getDerivationPath()))
|
||||
.filter(Optional::isPresent).map(Optional::get).findFirst();
|
||||
if(optInvalidScriptType.isPresent()) {
|
||||
ScriptType invalidScriptType = optInvalidScriptType.get();
|
||||
boolean includePolicyType = !wallet.getScriptType().getAllowedPolicyTypes().getFirst().equals(invalidScriptType.getAllowedPolicyTypes().getFirst());
|
||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Invalid derivation path", "This wallet is using the derivation path for " +
|
||||
invalidScriptType.getDescription(includePolicyType) + ", instead of the derivation path for its defined script type of " + wallet.getScriptType().getDescription(includePolicyType) +
|
||||
". \n\nDisable derivation path validation to import this wallet?", ButtonType.NO, ButtonType.YES);
|
||||
if(optType.isPresent()) {
|
||||
if(optType.get() == ButtonType.YES) {
|
||||
Config.get().setValidateDerivationPaths(false);
|
||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(true));
|
||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(true));
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static final List<Network> WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
|
||||
public static boolean isWhirlpoolCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().get(0).hasSeed()
|
||||
@@ -1093,8 +1248,10 @@ public class AppServices {
|
||||
|
||||
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1;
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().getFirst().getWalletModel() != WalletModel.BITBOX_02; //BitBox02 does not support high account numbers
|
||||
}
|
||||
|
||||
public static List<Wallet> addWhirlpoolWallets(Wallet decryptedWallet, String walletId, Storage storage) {
|
||||
@@ -1111,16 +1268,38 @@ public class AppServices {
|
||||
}
|
||||
|
||||
public static Font getMonospaceFont() {
|
||||
return Font.font("Roboto Mono", 13);
|
||||
return Font.font("Fragment Mono Regular", 13);
|
||||
}
|
||||
|
||||
public static boolean isOnWayland() {
|
||||
if(OsType.getCurrent() != OsType.UNIX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String waylandDisplay = System.getenv("WAYLAND_DISPLAY");
|
||||
return waylandDisplay != null && !waylandDisplay.isEmpty();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newConnection(ConnectionEvent event) {
|
||||
currentBlockHeight = event.getBlockHeight();
|
||||
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
|
||||
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
|
||||
if(getConfiguredMinimumRelayFeeRate(Config.get()) == null) {
|
||||
minimumRelayFeeRate = event.getMinimumRelayFeeRate() == null ? Transaction.DEFAULT_MIN_RELAY_FEE : event.getMinimumRelayFeeRate();
|
||||
}
|
||||
serverMinimumRelayFeeRate = event.getMinimumRelayFeeRate();
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
Config.get().addRecentServer();
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
if(feeRatesSource.supportsNetwork(Network.get()) && feeRatesSource.isExternal()) {
|
||||
fetchFeeRates();
|
||||
}
|
||||
|
||||
if(!blockSummaries.containsKey(currentBlockHeight)) {
|
||||
fetchBlockSummaries(Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1135,11 +1314,22 @@ public class AppServices {
|
||||
latestBlockHeader = event.getBlockHeader();
|
||||
String status = "Updating to new block height " + event.getHeight();
|
||||
EventManager.get().post(new StatusEvent(status));
|
||||
newBlockSubject.onNext(event);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void blockSummary(BlockSummaryEvent event) {
|
||||
blockSummaries.putAll(event.getBlockSummaryMap());
|
||||
if(AppServices.currentBlockHeight != null) {
|
||||
blockSummaries.keySet().removeIf(height -> AppServices.currentBlockHeight - height > 5);
|
||||
}
|
||||
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void feesUpdated(FeeRatesUpdatedEvent event) {
|
||||
targetBlockFeeRates = event.getTargetBlockFeeRates();
|
||||
nextBlockMedianFeeRate = event.getNextBlockMedianFeeRate();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1152,10 +1342,8 @@ public class AppServices {
|
||||
@Subscribe
|
||||
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
||||
//Perform once-off fee rates retrieval to immediately change displayed rates
|
||||
if(feeRatesService != null && !feeRatesService.isRunning() && Config.get().getMode() != Mode.OFFLINE) {
|
||||
feeRatesService = createFeeRatesService();
|
||||
feeRatesService.start();
|
||||
}
|
||||
fetchFeeRates();
|
||||
fetchBlockSummaries(Collections.emptyList());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1296,10 +1484,28 @@ public class AppServices {
|
||||
@Subscribe
|
||||
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
|
||||
String currentName = Config.get().getServerDisplayName();
|
||||
onlineProperty.set(false);
|
||||
log.warn("Failed to fetch wallet history from " + Config.get().getServerDisplayName() + ", reconnecting to another server...");
|
||||
Config.get().changePublicServer();
|
||||
boolean changed = changePublicServer();
|
||||
if(changed) {
|
||||
log.warn("Failed to fetch wallet history from " + currentName + ", reconnecting to another server...");
|
||||
} else {
|
||||
log.warn("Failed to fetch wallet history from " + currentName + ", retrying later");
|
||||
connectionService.setDelay(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
|
||||
EventManager.get().post(new StatusEvent("Wallet load failed: No other public servers available that can serve the open wallets, retrying later..."));
|
||||
}
|
||||
onlineProperty.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void silentPaymentsUnsubscribe(SilentPaymentsUnsubscribeEvent event) {
|
||||
if(isConnected()) {
|
||||
ElectrumServer.SilentPaymentsUnsubscribeService unsubscribeService = new ElectrumServer.SilentPaymentsUnsubscribeService(event.getScanAddress());
|
||||
unsubscribeService.setOnFailed(workerStateEvent -> {
|
||||
log.warn("Failed to unsubscribe silent payments for " + event.getScanAddress().getAddress(), workerStateEvent.getSource().getException());
|
||||
});
|
||||
unsubscribeService.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptChunk;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
@@ -54,7 +55,7 @@ public abstract class BaseController {
|
||||
descriptorArea.setMouseOverTextDelay(Duration.ofMillis(150));
|
||||
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
|
||||
TwoDimensional.Position position = descriptorArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
|
||||
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
|
||||
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_HD || descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_SP ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
|
||||
if(position.getMajor() > 0 && index >= 0 && index < descriptorArea.getWallet().getKeystores().size()) {
|
||||
Keystore hoverKeystore = descriptorArea.getWallet().getKeystores().get(index);
|
||||
Point2D pos = e.getScreenPosition();
|
||||
@@ -72,10 +73,13 @@ public abstract class BaseController {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("[");
|
||||
builder.append(keystore.getKeyDerivation().getMasterFingerprint());
|
||||
builder.append("/");
|
||||
builder.append(keystore.getKeyDerivation().getDerivationPath().replaceFirst("^m?/", ""));
|
||||
builder.append(KeyDerivation.writePath(KeyDerivation.parsePath(keystore.getKeyDerivation().getDerivationPath())).substring(1));
|
||||
builder.append("]");
|
||||
builder.append(keystore.getExtendedPublicKey().toString());
|
||||
if(keystore.getExtendedPublicKey() != null) {
|
||||
builder.append(keystore.getExtendedPublicKey().toString());
|
||||
} else if(keystore.getSilentPaymentScanAddress() != null) {
|
||||
builder.append(keystore.getSilentPaymentScanAddress().toKeyString());
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
public class BlockSummary implements Comparable<BlockSummary> {
|
||||
private final Integer height;
|
||||
private final Date timestamp;
|
||||
private final Double medianFee;
|
||||
private final Integer transactionCount;
|
||||
private final Integer weight;
|
||||
|
||||
public BlockSummary(Integer height, Date timestamp) {
|
||||
this(height, timestamp, null, null, null);
|
||||
}
|
||||
|
||||
public BlockSummary(Integer height, Date timestamp, Double medianFee, Integer transactionCount, Integer weight) {
|
||||
this.height = height;
|
||||
this.timestamp = timestamp;
|
||||
this.medianFee = medianFee;
|
||||
this.transactionCount = transactionCount;
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
public Integer getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public Date getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public Optional<Double> getMedianFee() {
|
||||
return medianFee == null ? Optional.empty() : Optional.of(medianFee);
|
||||
}
|
||||
|
||||
public Optional<Integer> getTransactionCount() {
|
||||
return transactionCount == null ? Optional.empty() : Optional.of(transactionCount);
|
||||
}
|
||||
|
||||
public Optional<Integer> getWeight() {
|
||||
return weight == null ? Optional.empty() : Optional.of(weight);
|
||||
}
|
||||
|
||||
private static long calculateElapsedSeconds(long timestampUtc) {
|
||||
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
||||
Instant nowInstant = Instant.now();
|
||||
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
|
||||
}
|
||||
|
||||
public String getElapsed() {
|
||||
long elapsed = calculateElapsedSeconds(getTimestamp().getTime());
|
||||
if(elapsed < 0) {
|
||||
return "now";
|
||||
} else if(elapsed < 60) {
|
||||
return elapsed + "s";
|
||||
} else if(elapsed < 3600) {
|
||||
return elapsed / 60 + "m";
|
||||
} else if(elapsed < 86400) {
|
||||
return elapsed / 3600 + "h";
|
||||
} else {
|
||||
return elapsed / 86400 + "d";
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return getElapsed() + ":" + getMedianFee();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(BlockSummary o) {
|
||||
return o.height - height;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
@@ -48,7 +49,7 @@ public class DefaultInteractionServices implements InteractionServices {
|
||||
}
|
||||
|
||||
String[] lines = content.split("\r\n|\r|\n");
|
||||
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
if(lines.length > 3 || OsType.getCurrent() == OsType.WINDOWS) {
|
||||
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
|
||||
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ public enum Interface {
|
||||
public static Interface get() {
|
||||
if(currentInterface == null) {
|
||||
boolean headless = java.awt.GraphicsEnvironment.isHeadless();
|
||||
boolean monocle = "Monocle".equalsIgnoreCase(System.getProperty("glass.platform"));
|
||||
boolean headlessPlatform = "Headless".equalsIgnoreCase(System.getProperty("glass.platform"));
|
||||
|
||||
if(headless || monocle) {
|
||||
if(headless || headlessPlatform) {
|
||||
currentInterface = TERMINAL;
|
||||
|
||||
if(headless && !monocle) {
|
||||
throw new UnsupportedOperationException("Headless environment detected but Monocle platform not found");
|
||||
if(headless && !headlessPlatform) {
|
||||
throw new UnsupportedOperationException("Headless environment detected but headless glass platform not found");
|
||||
}
|
||||
} else {
|
||||
currentInterface = DESKTOP;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.control.WalletIcon;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
@@ -9,13 +10,12 @@ import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.settings.SettingsGroup;
|
||||
import com.sparrowwallet.sparrow.settings.SettingsDialog;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.glyphfont.GlyphFontRegistry;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
@@ -42,10 +42,7 @@ public class SparrowDesktop extends Application {
|
||||
public void start(Stage stage) throws Exception {
|
||||
this.mainStage = stage;
|
||||
|
||||
GlyphFontRegistry.register(new FontAwesome5());
|
||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
|
||||
initializeFonts();
|
||||
URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null);
|
||||
|
||||
AppServices.initialize(this);
|
||||
@@ -60,8 +57,8 @@ public class SparrowDesktop extends Application {
|
||||
Config.get().setMode(mode);
|
||||
|
||||
if(mode.equals(Mode.ONLINE)) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
|
||||
Optional<Boolean> optNewWallet = preferencesDialog.showAndWait();
|
||||
SettingsDialog settingsDialog = new SettingsDialog(SettingsGroup.SERVER, true);
|
||||
Optional<Boolean> optNewWallet = settingsDialog.showAndWait();
|
||||
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
|
||||
} else if(Network.get() == Network.MAINNET) {
|
||||
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
|
||||
@@ -75,10 +72,6 @@ public class SparrowDesktop extends Application {
|
||||
Config.get().setServerType(ServerType.ELECTRUM_SERVER);
|
||||
}
|
||||
|
||||
if(Config.get().getHdCapture() == null && Platform.getCurrent() == Platform.OSX) {
|
||||
Config.get().setHdCapture(Boolean.TRUE);
|
||||
}
|
||||
|
||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_SCRIPT_TYPES_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
|
||||
System.setProperty(Wallet.ALLOW_DERIVATIONS_MATCHING_OTHER_NETWORKS_PROPERTY, Boolean.toString(!Config.get().isValidateDerivationPaths()));
|
||||
|
||||
@@ -89,28 +82,42 @@ public class SparrowDesktop extends Application {
|
||||
|
||||
AppController appController = AppServices.newAppWindow(stage);
|
||||
|
||||
if(createNewWallet) {
|
||||
appController.newWallet(null);
|
||||
}
|
||||
final boolean showNewWallet = createNewWallet;
|
||||
//Delay opening new dialogs on Wayland
|
||||
AppServices.runAfterDelay(AppServices.isOnWayland() ? 1000 : 0, () -> {
|
||||
if(showNewWallet) {
|
||||
appController.newWallet(null);
|
||||
}
|
||||
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null) {
|
||||
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
||||
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
||||
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
||||
sortedWalletFiles.addAll(encryptedWalletFiles);
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null) {
|
||||
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
||||
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
||||
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
||||
sortedWalletFiles.addAll(encryptedWalletFiles);
|
||||
|
||||
for(File walletFile : sortedWalletFiles) {
|
||||
if(walletFile.exists()) {
|
||||
appController.openWalletFile(walletFile, false);
|
||||
for(File walletFile : sortedWalletFiles) {
|
||||
if(walletFile.exists()) {
|
||||
appController.openWalletFile(walletFile, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
|
||||
|
||||
AppServices.get().start();
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeFonts() {
|
||||
GlyphFontRegistry.register(new FontAwesome5());
|
||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Regular.ttf"), 13);
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/FragmentMono-Italic.ttf"), 11);
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/LiberationSans-Regular.ttf"), 13);
|
||||
}
|
||||
|
||||
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
|
||||
|
||||
AppServices.get().start();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,14 +18,24 @@ import java.util.*;
|
||||
public class SparrowWallet {
|
||||
public static final String APP_ID = "sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.9.1";
|
||||
public static final String APP_VERSION = "2.5.1";
|
||||
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 static final String JPACKAGE_APP_PATH = "jpackage.app-path";
|
||||
|
||||
private static Instance instance;
|
||||
|
||||
public static void main(String[] argv) {
|
||||
if(System.getProperty(JPACKAGE_APP_PATH) != null) {
|
||||
String libDir = System.getProperty("java.home") + File.separator + "lib";
|
||||
System.setProperty("jna.boot.library.path", libDir);
|
||||
System.setProperty("jna.library.path", libDir);
|
||||
System.setProperty("jSerialComm.library.path", libDir);
|
||||
System.setProperty("org.usb4java.LibraryName", "usb4java");
|
||||
System.setProperty("java.library.path", libDir);
|
||||
}
|
||||
|
||||
Args args = new Args();
|
||||
JCommander jCommander = JCommander.newBuilder().addObject(args).programName(APP_NAME.toLowerCase(Locale.ROOT)).acceptUnknownOptions(true).build();
|
||||
jCommander.parse(argv);
|
||||
|
||||
@@ -21,7 +21,7 @@ public class WelcomeDialog extends Dialog<Mode> {
|
||||
welcomeController.initializeView();
|
||||
|
||||
dialogPane.setPrefWidth(600);
|
||||
dialogPane.setPrefHeight(520);
|
||||
dialogPane.setPrefHeight(540);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@@ -68,7 +69,7 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
|
||||
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
||||
if(!availableAccounts.isEmpty() && (masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||
|| (masterWallet.getPolicyType() == PolicyType.SINGLE_HD && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||
dialogPane.getButtonTypes().add(discoverButtonType);
|
||||
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
||||
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());
|
||||
|
||||
@@ -45,6 +45,7 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
|
||||
setTooltip(tooltip);
|
||||
getStyleClass().add("address-cell");
|
||||
|
||||
if(addressStatus.isDustAttack()) {
|
||||
setGraphic(getDustAttackHyperlink(utxoEntry));
|
||||
|
||||
@@ -22,6 +22,7 @@ public class AddressLabel extends IdLabel {
|
||||
|
||||
public AddressLabel(String text) {
|
||||
super(text);
|
||||
setSkin(new AddressTextFieldSkin(this));
|
||||
addressProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setAddressAsText(newValue);
|
||||
contextMenu.copyHex.setText("Copy " + newValue.getOutputScriptDataType());
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.skin.LabelSkin;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AddressLabelSkin extends LabelSkin {
|
||||
public static final int CHUNK_SIZE = 4;
|
||||
public static final Pattern CHUNK_PATTERN = Pattern.compile("(?<=\\G.{" + CHUNK_SIZE + "})");
|
||||
|
||||
private final TextFlow displayFlow;
|
||||
private final ChangeListener<String> textListener;
|
||||
private final ChangeListener<Font> fontListener;
|
||||
|
||||
public AddressLabelSkin(Label control) {
|
||||
super(control);
|
||||
|
||||
displayFlow = new TextFlow();
|
||||
displayFlow.setManaged(false);
|
||||
displayFlow.setMouseTransparent(true);
|
||||
|
||||
getChildren().addFirst(displayFlow);
|
||||
|
||||
textListener = (_, _, newText) -> updateDisplay(newText);
|
||||
fontListener = (_, _, _) -> updateDisplay(control.getText());
|
||||
control.textProperty().addListener(textListener);
|
||||
control.fontProperty().addListener(fontListener);
|
||||
updateDisplay(control.getText());
|
||||
|
||||
control.setStyle("-fx-text-fill: transparent;");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
getSkinnable().textProperty().removeListener(textListener);
|
||||
getSkinnable().fontProperty().removeListener(fontListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private void updateDisplay(String text) {
|
||||
displayFlow.getChildren().clear();
|
||||
if(text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<AddressSpan> addresses = findAddresses(text);
|
||||
|
||||
int pos = 0;
|
||||
for(AddressSpan span : addresses) {
|
||||
if(span.start > pos) {
|
||||
Text normalText = createText(text.substring(pos, span.start), false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
|
||||
addChunkedAddress(text.substring(span.start, span.end));
|
||||
pos = span.end;
|
||||
}
|
||||
|
||||
if(pos < text.length()) {
|
||||
Text normalText = createText(text.substring(pos), false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
}
|
||||
|
||||
private void addChunkedAddress(String address) {
|
||||
String[] chunks = CHUNK_PATTERN.split(address);
|
||||
for(int i = 0; i < chunks.length; i++) {
|
||||
Text chunk = createText(chunks[i], i % 2 != 0);
|
||||
displayFlow.getChildren().add(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private Text createText(String content, boolean alternate) {
|
||||
Text text = new Text(content);
|
||||
text.setFont(getSkinnable().getFont());
|
||||
text.getStyleClass().add("address-chunk");
|
||||
if(alternate) {
|
||||
text.getStyleClass().add("alternate");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private List<AddressSpan> findAddresses(String text) {
|
||||
List<AddressSpan> spans = new ArrayList<>();
|
||||
|
||||
Pattern wordBoundary = Pattern.compile("\\S+");
|
||||
Matcher matcher = wordBoundary.matcher(text);
|
||||
|
||||
while(matcher.find()) {
|
||||
String candidate = matcher.group();
|
||||
if(isValidAddress(candidate)) {
|
||||
spans.add(new AddressSpan(matcher.start(), matcher.end()));
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
private boolean isValidAddress(String candidate) {
|
||||
Network network = Network.get();
|
||||
return network.hasP2PKHAddressPrefix(candidate) || network.hasP2SHAddressPrefix(candidate) ||
|
||||
candidate.startsWith(network.getBech32AddressHRP()) || candidate.startsWith(network.getSilentPaymentsAddressHrp());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChildren() {
|
||||
super.updateChildren();
|
||||
if(displayFlow != null && !getChildren().contains(displayFlow)) {
|
||||
getChildren().addFirst(displayFlow);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
|
||||
// Position TextFlow to align with the label's text area
|
||||
Label label = getSkinnable();
|
||||
Insets padding = label.getPadding();
|
||||
|
||||
Node graphic = label.getGraphic();
|
||||
double graphicOffset = 0;
|
||||
if(graphic != null && label.getContentDisplay() == ContentDisplay.LEFT) {
|
||||
graphicOffset = graphic.getLayoutBounds().getWidth() + label.getGraphicTextGap();
|
||||
}
|
||||
|
||||
displayFlow.resizeRelocate(
|
||||
x + padding.getLeft() + graphicOffset,
|
||||
y + padding.getTop(),
|
||||
w - padding.getLeft() - padding.getRight() - graphicOffset,
|
||||
h - padding.getTop() - padding.getBottom()
|
||||
);
|
||||
}
|
||||
|
||||
private record AddressSpan(int start, int end) {}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.Base58;
|
||||
import com.sparrowwallet.drongo.protocol.Bech32;
|
||||
import impl.org.controlsfx.skin.CustomTextFieldSkin;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.shape.Path;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AddressTextFieldSkin extends CustomTextFieldSkin {
|
||||
private static final boolean[] BASE58_OK = buildOkTable(new String(Base58.ALPHABET));
|
||||
private static final boolean[] BECH32_DATA_OK = buildOkTable(Bech32.CHARSET);
|
||||
|
||||
private final TextFlow displayFlow;
|
||||
private final Rectangle clip;
|
||||
private final ChangeListener<String> textListener;
|
||||
private final ChangeListener<Font> fontListener;
|
||||
|
||||
public AddressTextFieldSkin(TextField control) {
|
||||
super(control);
|
||||
|
||||
displayFlow = new TextFlow();
|
||||
displayFlow.setManaged(false);
|
||||
displayFlow.setMouseTransparent(true);
|
||||
|
||||
clip = new Rectangle();
|
||||
displayFlow.setClip(clip);
|
||||
|
||||
getChildren().addFirst(displayFlow);
|
||||
|
||||
textListener = (_, _, newText) -> updateDisplay(newText);
|
||||
fontListener = (_, _, _) -> updateDisplay(control.getText());
|
||||
control.textProperty().addListener(textListener);
|
||||
control.fontProperty().addListener(fontListener);
|
||||
updateDisplay(control.getText());
|
||||
|
||||
control.setStyle("-fx-text-fill: transparent;");
|
||||
|
||||
// Unbind caret color since it's normally bound to textFill
|
||||
unbindCaretColor(getChildren());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
getSkinnable().textProperty().removeListener(textListener);
|
||||
getSkinnable().fontProperty().removeListener(fontListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private void unbindCaretColor(javafx.collections.ObservableList<Node> children) {
|
||||
for(Node node : children) {
|
||||
if(node instanceof Path path && path.getStroke() != null) {
|
||||
path.fillProperty().unbind();
|
||||
path.strokeProperty().unbind();
|
||||
path.getStyleClass().add("address-field-caret");
|
||||
} else if(node instanceof javafx.scene.Parent parent) {
|
||||
unbindCaretColor(parent.getChildrenUnmodifiable());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectProperty<Node> leftProperty() {
|
||||
if(getSkinnable() instanceof CustomTextField customTextField) {
|
||||
return customTextField.leftProperty();
|
||||
}
|
||||
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectProperty<Node> rightProperty() {
|
||||
if(getSkinnable() instanceof CustomTextField customTextField) {
|
||||
return customTextField.rightProperty();
|
||||
}
|
||||
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
private void updateDisplay(String text) {
|
||||
displayFlow.getChildren().clear();
|
||||
if(text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<AddressSpan> addresses = findAddresses(text);
|
||||
|
||||
int pos = 0;
|
||||
for(AddressSpan span : addresses) {
|
||||
if(span.start > pos) {
|
||||
Text normalText = createText(text.substring(pos, span.start), false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
|
||||
addChunkedAddress(text.substring(span.start, span.end));
|
||||
pos = span.end;
|
||||
}
|
||||
|
||||
if(pos < text.length()) {
|
||||
Text normalText = createText(text.substring(pos), false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
}
|
||||
|
||||
private void addChunkedAddress(String address) {
|
||||
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(address);
|
||||
for(int i = 0; i < chunks.length; i++) {
|
||||
Text chunk = createText(chunks[i], i % 2 != 0);
|
||||
displayFlow.getChildren().add(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
private Text createText(String content, boolean alternate) {
|
||||
Text text = new Text(content);
|
||||
text.setFont(getSkinnable().getFont());
|
||||
text.getStyleClass().add("address-chunk");
|
||||
if(alternate) {
|
||||
text.getStyleClass().add("alternate");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private List<AddressSpan> findAddresses(String text) {
|
||||
List<AddressSpan> spans = new ArrayList<>();
|
||||
|
||||
Pattern wordBoundary = Pattern.compile("\\S+");
|
||||
Matcher matcher = wordBoundary.matcher(text);
|
||||
|
||||
while(matcher.find()) {
|
||||
String candidate = matcher.group();
|
||||
if(isValidAddress(candidate)) {
|
||||
spans.add(new AddressSpan(matcher.start(), matcher.end()));
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
private boolean isValidAddress(String candidate) {
|
||||
if(candidate == null || candidate.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Network network = Network.get();
|
||||
|
||||
// Base58 (legacy) partial: must start with a legacy prefix and contain only base58 chars.
|
||||
if(network.hasP2PKHAddressPrefix(candidate) || network.hasP2SHAddressPrefix(candidate)) {
|
||||
return containsOnlyAscii(candidate, BASE58_OK);
|
||||
}
|
||||
|
||||
String lower = candidate.toLowerCase(Locale.ROOT);
|
||||
|
||||
// Bech32 (segwit v0/v1) partial: starts with HRP, then optional '1', then bech32 data charset.
|
||||
if(lower.startsWith(network.getBech32AddressHRP())) {
|
||||
return isBech32LikePartial(lower);
|
||||
}
|
||||
|
||||
// Silent payments partial (bech32-like): starts with its HRP, then optional '1', then bech32 data charset.
|
||||
if(lower.startsWith(network.getSilentPaymentsAddressHrp())) {
|
||||
return isBech32LikePartial(lower);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isBech32LikePartial(String lower) {
|
||||
int sep = lower.indexOf(Bech32.BECH32_SEPARATOR);
|
||||
|
||||
if(sep < 0) {
|
||||
return containsOnlyHrpChars(lower);
|
||||
}
|
||||
|
||||
String hrp = lower.substring(0, sep);
|
||||
String dataPart = lower.substring(sep + 1);
|
||||
|
||||
if(hrp.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return containsOnlyHrpChars(hrp) && containsOnlyAscii(dataPart, BECH32_DATA_OK);
|
||||
}
|
||||
|
||||
private static boolean containsOnlyHrpChars(String s) {
|
||||
for(int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
boolean ok = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
|
||||
if(!ok) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static boolean[] buildOkTable(String allowed) {
|
||||
boolean[] ok = new boolean[128];
|
||||
for(int i = 0; i < allowed.length(); i++) {
|
||||
char c = allowed.charAt(i);
|
||||
if(c < ok.length) {
|
||||
ok[c] = true;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Non-ASCII allowed char: " + c);
|
||||
}
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private static boolean containsOnlyAscii(String s, boolean[] ok) {
|
||||
for(int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if(c >= ok.length || !ok[c]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
|
||||
Insets padding = getSkinnable().getPadding();
|
||||
|
||||
double leftWidth = 0;
|
||||
double rightWidth = 0;
|
||||
if(getSkinnable() instanceof CustomTextField customTextField) {
|
||||
Node left = customTextField.getLeft();
|
||||
Node right = customTextField.getRight();
|
||||
if(left != null) {
|
||||
leftWidth = left.getLayoutBounds().getWidth();
|
||||
if(left instanceof Region leftRegion) {
|
||||
leftWidth += leftRegion.getPadding().getLeft() + leftRegion.getPadding().getRight() + 1;
|
||||
}
|
||||
}
|
||||
if(right != null) {
|
||||
rightWidth = right.getLayoutBounds().getWidth();
|
||||
if(right instanceof Region rightRegion) {
|
||||
rightWidth += rightRegion.getPadding().getLeft() + rightRegion.getPadding().getRight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double availableWidth = w - padding.getLeft() - padding.getRight() - leftWidth - rightWidth;
|
||||
clip.setWidth(availableWidth);
|
||||
clip.setHeight(h);
|
||||
|
||||
double topOffset = getSkinnable().getBaselineOffset() - displayFlow.getBaselineOffset();
|
||||
|
||||
displayFlow.resizeRelocate(
|
||||
padding.getLeft() + leftWidth,
|
||||
topOffset,
|
||||
displayFlow.prefWidth(-1),
|
||||
h - padding.getTop() - padding.getBottom()
|
||||
);
|
||||
}
|
||||
|
||||
private record AddressSpan(int start, int end) {}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AddressTooltipSkin implements Skin<Tooltip> {
|
||||
private final Tooltip tooltip;
|
||||
private final TextFlow textFlow;
|
||||
private final ChangeListener<String> textListener;
|
||||
|
||||
public AddressTooltipSkin(Tooltip tooltip) {
|
||||
this.tooltip = tooltip;
|
||||
|
||||
textFlow = new TextFlow();
|
||||
textFlow.getStyleClass().addAll(tooltip.getStyleClass());
|
||||
|
||||
textListener = (_, _, newText) -> updateDisplay(newText);
|
||||
tooltip.textProperty().addListener(textListener);
|
||||
updateDisplay(tooltip.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Tooltip getSkinnable() {
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node getNode() {
|
||||
return textFlow;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
tooltip.textProperty().removeListener(textListener);
|
||||
}
|
||||
|
||||
private void updateDisplay(String text) {
|
||||
textFlow.getChildren().clear();
|
||||
if(text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<AddressSpan> addresses = findAddresses(text);
|
||||
|
||||
int pos = 0;
|
||||
for(AddressSpan span : addresses) {
|
||||
if(span.start > pos) {
|
||||
textFlow.getChildren().add(createText(text.substring(pos, span.start), false));
|
||||
}
|
||||
addChunkedAddress(text.substring(span.start, span.end));
|
||||
pos = span.end;
|
||||
}
|
||||
|
||||
if(pos < text.length()) {
|
||||
textFlow.getChildren().add(createText(text.substring(pos), false));
|
||||
}
|
||||
}
|
||||
|
||||
private void addChunkedAddress(String address) {
|
||||
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(address);
|
||||
for(int i = 0; i < chunks.length; i++) {
|
||||
textFlow.getChildren().add(createText(chunks[i], i % 2 != 0));
|
||||
}
|
||||
}
|
||||
|
||||
private Text createText(String content, boolean alternate) {
|
||||
Text text = new Text(content);
|
||||
text.getStyleClass().add("address-chunk");
|
||||
if(alternate) {
|
||||
text.getStyleClass().add("alternate");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private List<AddressSpan> findAddresses(String text) {
|
||||
List<AddressSpan> spans = new ArrayList<>();
|
||||
|
||||
Pattern wordBoundary = Pattern.compile("\\S+");
|
||||
Matcher matcher = wordBoundary.matcher(text);
|
||||
|
||||
while(matcher.find()) {
|
||||
String candidate = matcher.group();
|
||||
if(isValidAddress(candidate)) {
|
||||
spans.add(new AddressSpan(matcher.start(), matcher.end()));
|
||||
}
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
||||
private boolean isValidAddress(String candidate) {
|
||||
try {
|
||||
Address.fromString(candidate);
|
||||
return true;
|
||||
} catch(InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private record AddressSpan(int start, int end) {}
|
||||
}
|
||||
@@ -30,7 +30,11 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
addressCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
|
||||
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
|
||||
});
|
||||
addressCol.setCellFactory(p -> new EntryCell());
|
||||
addressCol.setCellFactory(p -> {
|
||||
EntryCell entryCell = new EntryCell();
|
||||
entryCell.setSkin(new AddressTreeTableCellSkin<>(entryCell));
|
||||
return entryCell;
|
||||
});
|
||||
addressCol.setSortable(false);
|
||||
getColumns().add(addressCol);
|
||||
|
||||
@@ -76,8 +80,9 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
contextMenu.getItems().add(showCountItem);
|
||||
getColumns().forEach(col -> col.setContextMenu(contextMenu));
|
||||
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
setupColumnWidths();
|
||||
|
||||
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
|
||||
getSortOrder().add(addressCol);
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.scene.control.skin.TreeTableCellSkin;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
|
||||
public class AddressTreeTableCellSkin<S, T> extends TreeTableCellSkin<S, T> {
|
||||
private final TextFlow displayFlow;
|
||||
private final ChangeListener<String> textListener;
|
||||
private final Text ellipsisText;
|
||||
private String currentDisplayedText;
|
||||
|
||||
public AddressTreeTableCellSkin(TreeTableCell<S, T> cell) {
|
||||
super(cell);
|
||||
|
||||
displayFlow = new TextFlow();
|
||||
displayFlow.setManaged(false);
|
||||
displayFlow.setMouseTransparent(true);
|
||||
displayFlow.setMinWidth(Region.USE_PREF_SIZE);
|
||||
getChildren().add(displayFlow);
|
||||
|
||||
ellipsisText = new Text("...");
|
||||
ellipsisText.fontProperty().bind(cell.fontProperty());
|
||||
ellipsisText.getStyleClass().add("address-chunk");
|
||||
|
||||
textListener = (_, _, newText) -> updateDisplay(newText);
|
||||
cell.textProperty().addListener(textListener);
|
||||
updateDisplay(cell.getText());
|
||||
|
||||
cell.setStyle("-fx-text-fill: transparent;");
|
||||
}
|
||||
|
||||
private void updateDisplay(String text) {
|
||||
currentDisplayedText = text;
|
||||
buildDisplay(text, false);
|
||||
}
|
||||
|
||||
private void buildDisplay(String text, boolean truncated) {
|
||||
displayFlow.getChildren().clear();
|
||||
|
||||
if(text == null || text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(getSkinnable().getStyleClass().contains("address-cell")) {
|
||||
String[] chunks = AddressLabelSkin.CHUNK_PATTERN.split(text);
|
||||
for(int i = 0; i < chunks.length; i++) {
|
||||
displayFlow.getChildren().add(createText(chunks[i], i % 2 != 0));
|
||||
}
|
||||
} else {
|
||||
Text normalText = createText(text, false);
|
||||
displayFlow.getChildren().add(normalText);
|
||||
}
|
||||
|
||||
if(truncated) {
|
||||
displayFlow.getChildren().add(ellipsisText);
|
||||
}
|
||||
}
|
||||
|
||||
private Text createText(String content, boolean alternate) {
|
||||
Text text = new Text(content);
|
||||
text.fontProperty().bind(getSkinnable().fontProperty());
|
||||
text.getStyleClass().add("address-chunk");
|
||||
if(alternate) {
|
||||
text.getStyleClass().add("alternate");
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void layoutChildren(double x, double y, double w, double h) {
|
||||
super.layoutChildren(x, y, w, h);
|
||||
|
||||
TreeTableCell<S, T> cell = getSkinnable();
|
||||
Insets padding = cell.getPadding();
|
||||
|
||||
double leftOffset = 0;
|
||||
double topOffset = y + padding.getTop();
|
||||
|
||||
Text labeledText = (Text)getChildren().stream().filter(n -> n instanceof Text).findFirst().orElse(null);
|
||||
if(labeledText != null) {
|
||||
leftOffset = labeledText.getLayoutX();
|
||||
topOffset = labeledText.getLayoutY() - labeledText.getBaselineOffset();
|
||||
|
||||
String fullText = cell.getText();
|
||||
String displayedText = labeledText.getText();
|
||||
|
||||
if(fullText != null && displayedText != null && !fullText.equals(displayedText)) {
|
||||
String ellipsis = cell.getEllipsisString();
|
||||
if(displayedText.endsWith(ellipsis)) {
|
||||
String truncatedText = displayedText.substring(0, displayedText.length() - ellipsis.length());
|
||||
if(!truncatedText.equals(currentDisplayedText)) {
|
||||
currentDisplayedText = truncatedText;
|
||||
buildDisplay(truncatedText, true);
|
||||
}
|
||||
}
|
||||
} else if(fullText != null && !fullText.equals(currentDisplayedText)) {
|
||||
currentDisplayedText = fullText;
|
||||
buildDisplay(fullText, false);
|
||||
}
|
||||
}
|
||||
|
||||
displayFlow.resizeRelocate(
|
||||
leftOffset,
|
||||
topOffset,
|
||||
w - padding.getLeft() - padding.getRight(),
|
||||
h - padding.getTop() - padding.getBottom()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChildren() {
|
||||
super.updateChildren();
|
||||
if(displayFlow != null && !getChildren().contains(displayFlow)) {
|
||||
getChildren().add(displayFlow);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
getSkinnable().textProperty().removeListener(textListener);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -128,4 +128,15 @@ public class BalanceChart extends LineChart<Number, Number> {
|
||||
NumberAxis yaxis = (NumberAxis)getYAxis();
|
||||
yaxis.setTickLabelFormatter(new CoinAxisFormatter(yaxis, format, unit));
|
||||
}
|
||||
|
||||
public void refreshAxisLabels() {
|
||||
NumberAxis yaxis = (NumberAxis)getYAxis();
|
||||
// Force the axis to redraw by invalidating the upper and lower bounds
|
||||
yaxis.setAutoRanging(false);
|
||||
double lower = yaxis.getLowerBound();
|
||||
double upper = yaxis.getUpperBound();
|
||||
yaxis.setLowerBound(lower);
|
||||
yaxis.setUpperBound(upper);
|
||||
yaxis.setAutoRanging(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||
|
||||
public class BitBoxPairingDialog extends Alert {
|
||||
public BitBoxPairingDialog(String code) {
|
||||
super(AlertType.INFORMATION);
|
||||
initOwner(getActiveWindow());
|
||||
setStageIcon(getDialogPane().getScene().getWindow());
|
||||
getDialogPane().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
setTitle("Confirm BitBox02 Pairing");
|
||||
setHeaderText(getTitle());
|
||||
VBox vBox = new VBox(20);
|
||||
vBox.setAlignment(Pos.CENTER);
|
||||
vBox.setPadding(new Insets(10, 20, 10, 20));
|
||||
Label instructions = new Label("Confirm the following code is shown on BitBox02");
|
||||
Label codeLabel = new Label(code);
|
||||
codeLabel.getStyleClass().add("fixed-width");
|
||||
vBox.getChildren().addAll(instructions, codeLabel);
|
||||
getDialogPane().setContent(vBox);
|
||||
moveToActiveWindowScreen(this);
|
||||
getDialogPane().getButtonTypes().clear();
|
||||
getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.BlockSummary;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.KeyValue;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.Group;
|
||||
import javafx.scene.shape.Polygon;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.scene.text.FontWeight;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import javafx.util.Duration;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
|
||||
public class BlockCube extends Group {
|
||||
public static final List<Integer> MEMPOOL_FEE_RATES_INTERVALS = List.of(1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200, 250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000);
|
||||
|
||||
public static final double CUBE_SIZE = 60;
|
||||
|
||||
private final IntegerProperty weightProperty = new SimpleIntegerProperty(0);
|
||||
private final DoubleProperty medianFeeProperty = new SimpleDoubleProperty(-Double.MAX_VALUE);
|
||||
private final IntegerProperty heightProperty = new SimpleIntegerProperty(0);
|
||||
private final IntegerProperty txCountProperty = new SimpleIntegerProperty(0);
|
||||
private final LongProperty timestampProperty = new SimpleLongProperty(System.currentTimeMillis());
|
||||
private final StringProperty elapsedProperty = new SimpleStringProperty("");
|
||||
private final BooleanProperty confirmedProperty = new SimpleBooleanProperty(false);
|
||||
private final ObjectProperty<FeeRatesSource> feeRatesSource = new SimpleObjectProperty<>(null);
|
||||
|
||||
private Polygon front;
|
||||
private Rectangle unusedArea;
|
||||
private Rectangle usedArea;
|
||||
|
||||
private final Text heightText = new Text();
|
||||
private final Text medianFeeText = new Text();
|
||||
private final Text unitsText = new Text();
|
||||
private final TextFlow medianFeeTextFlow = new TextFlow();
|
||||
private final Text txCountText = new Text();
|
||||
private final Text elapsedText = new Text();
|
||||
private final Group feeRateIcon = new Group();
|
||||
|
||||
public BlockCube(Integer weight, Double medianFee, Integer height, Integer txCount, Long timestamp, boolean confirmed) {
|
||||
getStyleClass().addAll("block-" + Network.getCanonical().getName(), "block-cube");
|
||||
this.confirmedProperty.set(confirmed);
|
||||
|
||||
FeeRatesSource feeRatesSource = Config.get().getFeeRatesSource();
|
||||
feeRatesSource = (feeRatesSource == null ? FeeRatesSource.MEMPOOL_SPACE : feeRatesSource);
|
||||
this.feeRatesSource.set(feeRatesSource);
|
||||
|
||||
this.weightProperty.addListener((_, _, _) -> {
|
||||
if(front != null) {
|
||||
updateFill();
|
||||
}
|
||||
});
|
||||
this.medianFeeProperty.addListener((_, _, newValue) -> {
|
||||
medianFeeText.setText(newValue.doubleValue() < 0.0d ? "" : "~" + Math.round(Math.max(newValue.doubleValue(), 1.0d)));
|
||||
unitsText.setText(newValue.doubleValue() < 0.0d ? "" : " s/vb");
|
||||
double medianFeeWidth = TextUtils.computeTextWidth(medianFeeText.getFont(), medianFeeText.getText(), 0.0d);
|
||||
double unitsWidth = TextUtils.computeTextWidth(unitsText.getFont(), unitsText.getText(), 0.0d);
|
||||
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeWidth + unitsWidth)) / 2);
|
||||
});
|
||||
this.txCountProperty.addListener((_, _, newValue) -> {
|
||||
txCountText.setText(newValue.intValue() == 0 ? "" : newValue + " txes");
|
||||
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
||||
});
|
||||
this.timestampProperty.addListener((_, _, newValue) -> {
|
||||
elapsedProperty.set(getElapsed(newValue.longValue()));
|
||||
});
|
||||
this.elapsedProperty.addListener((_, _, newValue) -> {
|
||||
elapsedText.setText(isConfirmed() ? newValue : "In ~10m");
|
||||
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
|
||||
});
|
||||
this.heightProperty.addListener((_, _, newValue) -> {
|
||||
heightText.setText(newValue.intValue() == 0 ? "" : String.valueOf(newValue));
|
||||
heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2);
|
||||
});
|
||||
this.confirmedProperty.addListener((_, _, _) -> {
|
||||
if(front != null) {
|
||||
updateFill();
|
||||
}
|
||||
});
|
||||
this.feeRatesSource.addListener((_, _, _) -> {
|
||||
if(front != null) {
|
||||
updateFill();
|
||||
}
|
||||
});
|
||||
this.medianFeeText.textProperty().addListener((_, _, _) -> {
|
||||
pulse();
|
||||
});
|
||||
|
||||
if(weight != null) {
|
||||
this.weightProperty.set(weight);
|
||||
}
|
||||
if(medianFee != null) {
|
||||
this.medianFeeProperty.set(medianFee);
|
||||
}
|
||||
if(height != null) {
|
||||
this.heightProperty.set(height);
|
||||
}
|
||||
if(txCount != null) {
|
||||
this.txCountProperty.set(txCount);
|
||||
}
|
||||
if(timestamp != null) {
|
||||
this.timestampProperty.set(timestamp);
|
||||
}
|
||||
|
||||
drawCube();
|
||||
}
|
||||
|
||||
private void drawCube() {
|
||||
double depth = CUBE_SIZE * 0.2;
|
||||
double perspective = CUBE_SIZE * 0.04;
|
||||
|
||||
front = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE, CUBE_SIZE, 0, CUBE_SIZE);
|
||||
front.getStyleClass().add("block-front");
|
||||
front.setFill(null);
|
||||
unusedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE);
|
||||
unusedArea.getStyleClass().add("block-unused");
|
||||
usedArea = new Rectangle(0, 0, CUBE_SIZE, CUBE_SIZE);
|
||||
usedArea.getStyleClass().add("block-used");
|
||||
|
||||
Group frontFaceGroup = new Group(front, unusedArea, usedArea);
|
||||
|
||||
Polygon top = new Polygon(0, 0, CUBE_SIZE, 0, CUBE_SIZE - depth - perspective, -depth, -depth, -depth);
|
||||
top.getStyleClass().add("block-top");
|
||||
top.setStroke(null);
|
||||
|
||||
Polygon left = new Polygon(0, 0, -depth, -depth, -depth, CUBE_SIZE - depth - perspective, 0, CUBE_SIZE);
|
||||
left.getStyleClass().add("block-left");
|
||||
left.setStroke(null);
|
||||
|
||||
updateFill();
|
||||
|
||||
heightText.getStyleClass().add("block-height");
|
||||
heightText.setFont(new Font(11));
|
||||
heightText.setX(((CUBE_SIZE * 0.7) - heightText.getLayoutBounds().getWidth()) / 2);
|
||||
heightText.setY(-24);
|
||||
|
||||
medianFeeText.getStyleClass().add("block-text");
|
||||
medianFeeText.setFont(Font.font(null, FontWeight.BOLD, 11));
|
||||
unitsText.getStyleClass().add("block-text");
|
||||
unitsText.setFont(new Font(10));
|
||||
medianFeeTextFlow.getChildren().addAll(medianFeeText, unitsText);
|
||||
medianFeeTextFlow.setTranslateX((CUBE_SIZE - (medianFeeText.getLayoutBounds().getWidth() + unitsText.getLayoutBounds().getWidth())) / 2);
|
||||
medianFeeTextFlow.setTranslateY(7);
|
||||
|
||||
txCountText.getStyleClass().add("block-text");
|
||||
txCountText.setFont(new Font(10));
|
||||
txCountText.setOpacity(0.7);
|
||||
txCountText.setX((CUBE_SIZE - txCountText.getLayoutBounds().getWidth()) / 2);
|
||||
txCountText.setY(34);
|
||||
|
||||
feeRateIcon.setTranslateX(((CUBE_SIZE * 0.7) - 14) / 2);
|
||||
feeRateIcon.setTranslateY(-36);
|
||||
|
||||
elapsedText.getStyleClass().add("block-text");
|
||||
elapsedText.setFont(new Font(10));
|
||||
elapsedText.setX((CUBE_SIZE - elapsedText.getLayoutBounds().getWidth()) / 2);
|
||||
elapsedText.setY(50);
|
||||
|
||||
getChildren().addAll(frontFaceGroup, top, left, heightText, medianFeeTextFlow, txCountText, feeRateIcon, elapsedText);
|
||||
}
|
||||
|
||||
private void updateFill() {
|
||||
if(isConfirmed()) {
|
||||
getStyleClass().removeAll("block-unconfirmed");
|
||||
if(!getStyleClass().contains("block-confirmed")) {
|
||||
getStyleClass().add("block-confirmed");
|
||||
}
|
||||
double startY = 1 - weightProperty.doubleValue() / (Transaction.MAX_BLOCK_SIZE_VBYTES * Transaction.WITNESS_SCALE_FACTOR);
|
||||
double startYAbsolute = startY * BlockCube.CUBE_SIZE;
|
||||
unusedArea.setHeight(startYAbsolute);
|
||||
unusedArea.setStyle(null);
|
||||
usedArea.setY(startYAbsolute);
|
||||
usedArea.setHeight(CUBE_SIZE - startYAbsolute);
|
||||
usedArea.setVisible(true);
|
||||
heightText.setVisible(true);
|
||||
feeRateIcon.getChildren().clear();
|
||||
} else {
|
||||
getStyleClass().removeAll("block-confirmed");
|
||||
if(!getStyleClass().contains("block-unconfirmed")) {
|
||||
getStyleClass().add("block-unconfirmed");
|
||||
}
|
||||
usedArea.setVisible(false);
|
||||
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
||||
heightText.setVisible(false);
|
||||
if(feeRatesSource.get() != null) {
|
||||
SVGImage svgImage = feeRatesSource.get().getSVGImage();
|
||||
if(svgImage != null) {
|
||||
feeRateIcon.getChildren().setAll(feeRatesSource.get().getSVGImage());
|
||||
} else {
|
||||
feeRateIcon.getChildren().clear();
|
||||
}
|
||||
} else {
|
||||
feeRateIcon.getChildren().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void pulse() {
|
||||
if(isConfirmed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(unusedArea != null) {
|
||||
unusedArea.setStyle("-fx-fill: " + getFeeRateStyleName() + ";");
|
||||
}
|
||||
|
||||
Timeline timeline = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(opacityProperty(), 1.0)),
|
||||
new KeyFrame(Duration.millis(500), new KeyValue(opacityProperty(), 0.7)),
|
||||
new KeyFrame(Duration.millis(1000), new KeyValue(opacityProperty(), 1.0))
|
||||
);
|
||||
|
||||
timeline.setCycleCount(1);
|
||||
timeline.play();
|
||||
}
|
||||
|
||||
private static long calculateElapsedSeconds(long timestampUtc) {
|
||||
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
||||
Instant nowInstant = Instant.now();
|
||||
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
|
||||
}
|
||||
|
||||
public static String getElapsed(long timestampUtc) {
|
||||
long elapsed = calculateElapsedSeconds(timestampUtc);
|
||||
if(elapsed < 60) {
|
||||
return "Just now";
|
||||
} else if(elapsed < 3600) {
|
||||
return Math.round(elapsed / 60f) + "m ago";
|
||||
} else if(elapsed < 86400) {
|
||||
return Math.round(elapsed / 3600f) + "h ago";
|
||||
} else {
|
||||
return Math.round(elapsed / 86400d) + "d ago";
|
||||
}
|
||||
}
|
||||
|
||||
private String getFeeRateStyleName() {
|
||||
double rate = getMedianFee();
|
||||
int[] feeRateInterval = getFeeRateInterval(rate);
|
||||
if(feeRateInterval[1] == Integer.MAX_VALUE) {
|
||||
return "VSIZE2000-2200_COLOR";
|
||||
}
|
||||
int[] nextRateInterval = getFeeRateInterval(rate * 2);
|
||||
String from = "VSIZE" + feeRateInterval[0] + "-" + feeRateInterval[1] + "_COLOR";
|
||||
String to = "VSIZE" + nextRateInterval[0] + "-" + (nextRateInterval[1] == Integer.MAX_VALUE ? "2200" : nextRateInterval[1]) + "_COLOR";
|
||||
return "linear-gradient(from 75% 0% to 100% 0%, " + from + " 0%, " + to + " 100%, " + from +")";
|
||||
}
|
||||
|
||||
private int[] getFeeRateInterval(double medianFee) {
|
||||
for(int i = 0; i < MEMPOOL_FEE_RATES_INTERVALS.size(); i++) {
|
||||
int feeRate = MEMPOOL_FEE_RATES_INTERVALS.get(i);
|
||||
int nextFeeRate = (i == MEMPOOL_FEE_RATES_INTERVALS.size() - 1 ? Integer.MAX_VALUE : MEMPOOL_FEE_RATES_INTERVALS.get(i + 1));
|
||||
if(feeRate <= medianFee && nextFeeRate > medianFee) {
|
||||
return new int[] { feeRate, nextFeeRate };
|
||||
}
|
||||
}
|
||||
|
||||
return new int[] { 1, 2 };
|
||||
}
|
||||
|
||||
public int getWeight() {
|
||||
return weightProperty.get();
|
||||
}
|
||||
|
||||
public IntegerProperty weightProperty() {
|
||||
return weightProperty;
|
||||
}
|
||||
|
||||
public void setWeight(int weight) {
|
||||
weightProperty.set(weight);
|
||||
}
|
||||
|
||||
public double getMedianFee() {
|
||||
return medianFeeProperty.get();
|
||||
}
|
||||
|
||||
public DoubleProperty medianFee() {
|
||||
return medianFeeProperty;
|
||||
}
|
||||
|
||||
public void setMedianFee(double medianFee) {
|
||||
medianFeeProperty.set(medianFee);
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return heightProperty.get();
|
||||
}
|
||||
|
||||
public IntegerProperty heightProperty() {
|
||||
return heightProperty;
|
||||
}
|
||||
|
||||
public void setHeight(int height) {
|
||||
heightProperty.set(height);
|
||||
}
|
||||
|
||||
public int getTxCount() {
|
||||
return txCountProperty.get();
|
||||
}
|
||||
|
||||
public IntegerProperty txCountProperty() {
|
||||
return txCountProperty;
|
||||
}
|
||||
|
||||
public void setTxCount(int txCount) {
|
||||
txCountProperty.set(txCount);
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestampProperty.get();
|
||||
}
|
||||
|
||||
public LongProperty timestampProperty() {
|
||||
return timestampProperty;
|
||||
}
|
||||
|
||||
public void setTimestamp(long timestamp) {
|
||||
timestampProperty.set(timestamp);
|
||||
}
|
||||
|
||||
public String getElapsed() {
|
||||
return elapsedProperty.get();
|
||||
}
|
||||
|
||||
public StringProperty elapsedProperty() {
|
||||
return elapsedProperty;
|
||||
}
|
||||
|
||||
public void setElapsed(String elapsed) {
|
||||
elapsedProperty.set(elapsed);
|
||||
}
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmedProperty.get();
|
||||
}
|
||||
|
||||
public BooleanProperty confirmedProperty() {
|
||||
return confirmedProperty;
|
||||
}
|
||||
|
||||
public void setConfirmed(boolean confirmed) {
|
||||
confirmedProperty.set(confirmed);
|
||||
}
|
||||
|
||||
public FeeRatesSource getFeeRatesSource() {
|
||||
return feeRatesSource.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<FeeRatesSource> feeRatesSourceProperty() {
|
||||
return feeRatesSource;
|
||||
}
|
||||
|
||||
public void setFeeRatesSource(FeeRatesSource feeRatesSource) {
|
||||
this.feeRatesSource.set(feeRatesSource);
|
||||
}
|
||||
|
||||
public static BlockCube fromBlockSummary(BlockSummary blockSummary) {
|
||||
return new BlockCube(blockSummary.getWeight().orElse(0), blockSummary.getMedianFee().orElse(-1.0d), blockSummary.getHeight(),
|
||||
blockSummary.getTransactionCount().orElse(0), blockSummary.getTimestamp().getTime(), true);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@@ -43,14 +45,24 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(CardImportPane.class);
|
||||
|
||||
private final KeystoreCardImport importer;
|
||||
private final PolicyType policyType;
|
||||
private List<ChildNumber> derivation;
|
||||
protected Button importButton;
|
||||
private final SimpleStringProperty pin = new SimpleStringProperty("");
|
||||
|
||||
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation requiredDerivation) {
|
||||
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
public CardImportPane(Wallet wallet, KeystoreCardImport importer, KeyDerivation defaultDerivation, KeyDerivation requiredDerivation) {
|
||||
super(importer.getName(), "Place card on reader", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel());
|
||||
this.importer = importer;
|
||||
this.derivation = requiredDerivation == null ? wallet.getScriptType().getDefaultDerivation() : requiredDerivation.getDerivation();
|
||||
this.policyType = wallet.getPolicyType();
|
||||
this.derivation = requiredDerivation == null ? getDefaultDerivation(wallet, defaultDerivation) : requiredDerivation.getDerivation();
|
||||
}
|
||||
|
||||
private static List<ChildNumber> getDefaultDerivation(Wallet wallet, KeyDerivation defaultDerivation) {
|
||||
if(defaultDerivation != null && !defaultDerivation.getDerivation().isEmpty()) {
|
||||
return defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
return wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -102,7 +114,7 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
return;
|
||||
}
|
||||
|
||||
CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty);
|
||||
CardImportService cardImportService = new CardImportService(importer, policyType, pin.get(), derivation, messageProperty);
|
||||
cardImportService.setOnSucceeded(event -> {
|
||||
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
|
||||
});
|
||||
@@ -343,12 +355,14 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
|
||||
public static class CardImportService extends Service<Keystore> {
|
||||
private final KeystoreCardImport cardImport;
|
||||
private final PolicyType policyType;
|
||||
private final String pin;
|
||||
private final List<ChildNumber> derivation;
|
||||
private final StringProperty messageProperty;
|
||||
|
||||
public CardImportService(KeystoreCardImport cardImport, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
public CardImportService(KeystoreCardImport cardImport, PolicyType policyType, String pin, List<ChildNumber> derivation, StringProperty messageProperty) {
|
||||
this.cardImport = cardImport;
|
||||
this.policyType = policyType;
|
||||
this.pin = pin;
|
||||
this.derivation = derivation;
|
||||
this.messageProperty = messageProperty;
|
||||
@@ -359,7 +373,7 @@ public class CardImportPane extends TitledDescriptionPane {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Keystore call() throws Exception {
|
||||
return cardImport.getKeystore(pin, derivation, messageProperty);
|
||||
return cardImport.getKeystore(policyType, pin, derivation, messageProperty);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
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.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreCodexImport;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.SplitMenuButton;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class CodexKeystoreImportPane extends TitledDescriptionPane {
|
||||
protected final Wallet wallet;
|
||||
private final KeystoreCodexImport importer;
|
||||
private final KeyDerivation defaultDerivation;
|
||||
|
||||
private SplitMenuButton importButton;
|
||||
|
||||
private Button enterCodexButton;
|
||||
private Button calculateButton;
|
||||
|
||||
protected Label validLabel;
|
||||
protected Label invalidLabel;
|
||||
|
||||
protected final SimpleStringProperty secretShareProperty = new SimpleStringProperty("");
|
||||
|
||||
public CodexKeystoreImportPane(Wallet wallet, KeystoreCodexImport importer, KeyDerivation defaultDerivation) {
|
||||
super(importer.getName(), "Enter secret share", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
|
||||
createImportButton();
|
||||
buttonBox.getChildren().add(importButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
enterCodexButton = new Button("Enter Secret Share");
|
||||
enterCodexButton.managedProperty().bind(enterCodexButton.visibleProperty());
|
||||
enterCodexButton.setOnAction(event -> {
|
||||
enterCodex();
|
||||
});
|
||||
|
||||
return enterCodexButton;
|
||||
}
|
||||
|
||||
private void enterCodex() {
|
||||
setDescription("Enter secret share");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getSecretShareEntry());
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
private void importKeystore(List<ChildNumber> derivation) {
|
||||
importButton.setDisable(true);
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, secretShareProperty.get());
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
} catch(ImportException 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);
|
||||
}
|
||||
}
|
||||
|
||||
private void createImportButton() {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
setDefaultButton(importButton);
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(getDefaultDerivation());
|
||||
});
|
||||
String[] accounts = new String[]{"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importButton.getItems().add(item);
|
||||
}
|
||||
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
importButton.setVisible(false);
|
||||
}
|
||||
|
||||
private List<ChildNumber> getDefaultDerivation() {
|
||||
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
private void onInputChange(boolean empty, boolean validChecksum) {
|
||||
if(!empty) {
|
||||
try {
|
||||
importer.getKeystore(wallet.getPolicyType(), ScriptType.P2WPKH.getDefaultDerivation(), secretShareProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
invalidLabel.setText("Invalid checksum");
|
||||
invalidLabel.setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
calculateButton.setDisable(!validChecksum);
|
||||
validLabel.setVisible(validChecksum);
|
||||
invalidLabel.setVisible(!validChecksum && !empty);
|
||||
}
|
||||
|
||||
private Node getSecretShareEntry() {
|
||||
VBox vBox = new VBox(20);
|
||||
vBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
|
||||
HBox shareEntry = new HBox(10);
|
||||
shareEntry.setAlignment(Pos.CENTER_LEFT);
|
||||
Label shareLabel = new Label("Secret:");
|
||||
TextField shareField = new TextField();
|
||||
HBox.setHgrow(shareField, Priority.ALWAYS);
|
||||
shareField.setPromptText("ms...");
|
||||
shareField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
secretShareProperty.set(newValue);
|
||||
});
|
||||
shareEntry.getChildren().addAll(shareLabel, shareField);
|
||||
vBox.getChildren().add(shareEntry);
|
||||
|
||||
AnchorPane buttonPane = new AnchorPane();
|
||||
|
||||
validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph());
|
||||
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", GlyphUtils.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);
|
||||
|
||||
secretShareProperty.addListener((ChangeListener<String>) (c, oldval, newval) -> {
|
||||
boolean empty = secretShareProperty.isEmpty().get();
|
||||
boolean validChecksum = false;
|
||||
onInputChange(empty, validChecksum);
|
||||
});
|
||||
|
||||
HBox rightBox = new HBox();
|
||||
rightBox.setSpacing(10);
|
||||
|
||||
calculateButton = new Button("Create Keystore");
|
||||
calculateButton.setDisable(true);
|
||||
calculateButton.setDefaultButton(true);
|
||||
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
|
||||
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided secret share"));
|
||||
calculateButton.setOnAction(event -> {
|
||||
setExpanded(true);
|
||||
enterCodexButton.setVisible(false);
|
||||
importButton.setVisible(true);
|
||||
importButton.setDisable(false);
|
||||
setDescription("Ready to import");
|
||||
showHideLink.setText("Show Derivation...");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getDerivationEntry(getDefaultDerivation()));
|
||||
});
|
||||
|
||||
rightBox.getChildren().add(calculateButton);
|
||||
|
||||
buttonPane.getChildren().add(rightBox);
|
||||
AnchorPane.setRightAnchor(rightBox, 0.0);
|
||||
|
||||
vBox.getChildren().add(buttonPane);
|
||||
|
||||
Platform.runLater(shareField::requestFocus);
|
||||
|
||||
return vBox;
|
||||
}
|
||||
|
||||
private Node getDerivationEntry(List<ChildNumber> derivation) {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(derivationField, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
|
||||
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
|
||||
importDerivationButton.setDisable(true);
|
||||
importDerivationButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
|
||||
importKeystore(importDerivation);
|
||||
});
|
||||
|
||||
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().add(derivationField);
|
||||
contentBox.getChildren().add(importDerivationButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
@@ -18,6 +19,10 @@ final class CoinAxisFormatter extends StringConverter<Number> {
|
||||
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
if(Config.get().isHideAmounts()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Double value = bitcoinUnit.getValue(object.longValue());
|
||||
return new CoinTextFormatter(unitFormat).getCoinFormat().format(value);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
@@ -16,7 +17,6 @@ import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
@@ -32,7 +32,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
tooltip.setShowDelay(Duration.millis(500));
|
||||
contextMenu = new CoinContextMenu();
|
||||
getStyleClass().add("coin-cell");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
getStyleClass().add("number-field");
|
||||
}
|
||||
}
|
||||
@@ -58,16 +58,22 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
DecimalFormat decimalFormat = (amount.longValue() == 0L ? format.getBtcFormat() : format.getTableBtcFormat());
|
||||
final String btcValue = decimalFormat.format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
|
||||
setText(btcValue);
|
||||
if(Config.get().isHideAmounts()) {
|
||||
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
|
||||
setTooltip(null);
|
||||
setContextMenu(null);
|
||||
} else {
|
||||
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
|
||||
setText(satsValue);
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setValue(satsValue + " " + BitcoinUnit.SATOSHIS.getLabel());
|
||||
setText(btcValue);
|
||||
} else {
|
||||
tooltip.setValue(btcValue + " " + BitcoinUnit.BTC.getLabel());
|
||||
setText(satsValue);
|
||||
}
|
||||
setTooltip(tooltip);
|
||||
contextMenu.updateAmount(amount);
|
||||
setContextMenu(contextMenu);
|
||||
}
|
||||
setTooltip(tooltip);
|
||||
contextMenu.updateAmount(amount);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
tooltip.showConfirmations(transactionEntry.confirmationsProperty(), transactionEntry.isCoinbase());
|
||||
@@ -86,14 +92,16 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
}
|
||||
} else if(entry instanceof UtxoEntry) {
|
||||
setGraphic(null);
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
tooltip.hideConfirmations();
|
||||
|
||||
Region node = new Region();
|
||||
node.setPrefWidth(10);
|
||||
setGraphic(node);
|
||||
setContentDisplay(ContentDisplay.RIGHT);
|
||||
|
||||
if(((HashIndexEntry) entry).getType() == HashIndexEntry.Type.INPUT) {
|
||||
satsValue = "-" + satsValue;
|
||||
if(hashIndexEntry.getType() == HashIndexEntry.Type.INPUT && !Config.get().isHideAmounts()) {
|
||||
setText("-" + getText());
|
||||
}
|
||||
} else {
|
||||
setGraphic(null);
|
||||
@@ -148,6 +156,14 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
setTooltipText();
|
||||
}
|
||||
|
||||
public void hideConfirmations() {
|
||||
showConfirmations = false;
|
||||
isCoinbase = false;
|
||||
confirmationsProperty.unbind();
|
||||
|
||||
setTooltipText();
|
||||
}
|
||||
|
||||
private void setTooltipText() {
|
||||
setText(value + (showConfirmations ? " (" + getConfirmationsDescription() + ")" : ""));
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
|
||||
public class CoinLabel extends Label {
|
||||
public static final String HIDDEN_AMOUNT_TEXT = "\u2022\u2022\u2022\u2022\u2022";
|
||||
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
private final CoinContextMenu contextMenu;
|
||||
@@ -49,6 +51,13 @@ public class CoinLabel extends Label {
|
||||
}
|
||||
|
||||
private void setValueAsText(Long value, BitcoinUnit bitcoinUnit) {
|
||||
if(Config.get().isHideAmounts()) {
|
||||
setText(HIDDEN_AMOUNT_TEXT);
|
||||
setTooltip(null);
|
||||
setContextMenu(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import javafx.scene.control.TextFormatter;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.text.ParseException;
|
||||
import java.util.function.UnaryOperator;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class CoinTextFormatter extends TextFormatter<String> {
|
||||
@@ -51,8 +51,14 @@ public class CoinTextFormatter extends TextFormatter<String> {
|
||||
commasRemoved = newText.length() - noFractionCommaText.length();
|
||||
}
|
||||
|
||||
if(!coinValidation.matcher(noFractionCommaText).matches()) {
|
||||
return null;
|
||||
Matcher matcher = coinValidation.matcher(noFractionCommaText);
|
||||
if(!matcher.matches()) {
|
||||
matcher.reset();
|
||||
if(matcher.find()) {
|
||||
noFractionCommaText = matcher.group();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if(unitFormat.getGroupingSeparator().equals(change.getText())) {
|
||||
|
||||
@@ -1,36 +1,58 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.SortDirection;
|
||||
import com.sparrowwallet.drongo.wallet.TableType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTable;
|
||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletTableChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.BitcoindClient;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TreeTableColumn;
|
||||
import javafx.scene.control.TreeTableView;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
private TableType tableType;
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
private UnitFormat unitFormat;
|
||||
private CurrencyRate currencyRate;
|
||||
protected static final double STANDARD_WIDTH = 100.0;
|
||||
|
||||
private final PublishSubject<WalletTableChangedEvent> walletTableSubject = PublishSubject.create();
|
||||
private final Observable<WalletTableChangedEvent> walletTableEvents = walletTableSubject.debounce(1, TimeUnit.SECONDS);
|
||||
|
||||
public TableType getTableType() {
|
||||
return tableType;
|
||||
}
|
||||
|
||||
public void setTableType(TableType tableType) {
|
||||
this.tableType = tableType;
|
||||
}
|
||||
|
||||
public BitcoinUnit getBitcoinUnit() {
|
||||
return bitcoinUnit;
|
||||
@@ -88,7 +110,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
setPlaceholder(new Label("Error loading transactions: " + event.getErrorMessage()));
|
||||
} else if(event.isLoading()) {
|
||||
if(event.getStatusMessage() != null) {
|
||||
setPlaceholder(new Label(event.getStatusMessage() + "..."));
|
||||
setPlaceholder(new Label(event.getStatusMessage() + (event.getStatusMessage().contains("...") ? "" : "...")));
|
||||
} else {
|
||||
setPlaceholder(new Label("Loading transactions..."));
|
||||
}
|
||||
@@ -104,7 +126,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions"));
|
||||
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && !AppServices.isConnecting()) {
|
||||
if((Config.get().getServerType() == ServerType.BITCOIN_CORE || wallet.getPolicyType() == PolicyType.SINGLE_SP) && !AppServices.isConnecting() && !isFullyScanned(wallet)) {
|
||||
Hyperlink hyperlink = new Hyperlink();
|
||||
hyperlink.setTranslateY(30);
|
||||
hyperlink.setOnAction(event -> {
|
||||
@@ -115,6 +137,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Wallet pastWallet = wallet.copy();
|
||||
wallet.setBirthDate(optDate.get());
|
||||
wallet.setBirthHeight(null);
|
||||
//Trigger background save of birthdate
|
||||
EventManager.get().post(new WalletDataChangedEvent(wallet));
|
||||
//Trigger full wallet rescan
|
||||
@@ -130,17 +153,146 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
}
|
||||
|
||||
stackPane.getChildren().add(hyperlink);
|
||||
} else if(!AppServices.isConnecting() && Config.get().getServerType() == ServerType.BITCOIN_CORE && isFullyScanned(wallet)) {
|
||||
Date prunedDate = getPrunedDate();
|
||||
if(prunedDate != null) {
|
||||
DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN);
|
||||
Label prunedLabel = new Label("Scanned to pruned start date of " + dateFormat.format(prunedDate));
|
||||
prunedLabel.setTranslateY(30);
|
||||
stackPane.getChildren().add(prunedLabel);
|
||||
}
|
||||
}
|
||||
|
||||
stackPane.setAlignment(Pos.CENTER);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
public void setSortColumn(int columnIndex, TreeTableColumn.SortType sortType) {
|
||||
if(columnIndex >= 0 && columnIndex < getColumns().size() && getSortOrder().isEmpty() && !getRoot().getChildren().isEmpty()) {
|
||||
TreeTableColumn<Entry, ?> column = getColumns().get(columnIndex);
|
||||
column.setSortType(sortType == null ? TreeTableColumn.SortType.DESCENDING : sortType);
|
||||
private boolean isFullyScanned(Wallet wallet) {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
return wallet.isValid() && ElectrumServer.isSilentPaymentsFullyCovered(wallet.getSilentPaymentScanAddress());
|
||||
}
|
||||
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
Date prunedDate = getPrunedDate();
|
||||
return prunedDate != null && wallet.getBirthDate() != null && !wallet.getBirthDate().after(prunedDate);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Date getPrunedDate() {
|
||||
Cormorant cormorant = ElectrumServer.getCormorant();
|
||||
if(cormorant == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BitcoindClient bitcoindClient = cormorant.getBitcoindClient();
|
||||
if(bitcoindClient == null || !bitcoindClient.isPruned()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bitcoindClient.getCachedPrunedDate();
|
||||
}
|
||||
|
||||
protected void setupColumnSort(int defaultColumnIndex, TreeTableColumn.SortType defaultSortType) {
|
||||
WalletTable.Sort columnSort = getSavedColumnSort();
|
||||
if(columnSort == null) {
|
||||
columnSort = new WalletTable.Sort(defaultColumnIndex, getSortDirection(defaultSortType));
|
||||
}
|
||||
|
||||
setSortColumn(columnSort);
|
||||
|
||||
getSortOrder().addListener((ListChangeListener<? super TreeTableColumn<Entry, ?>>) c -> {
|
||||
if(c.next()) {
|
||||
walletTableChanged();
|
||||
}
|
||||
});
|
||||
for(TreeTableColumn<Entry, ?> column : getColumns()) {
|
||||
column.sortTypeProperty().addListener((_, _, _) -> walletTableChanged());
|
||||
}
|
||||
}
|
||||
|
||||
protected void resetSortColumn() {
|
||||
setSortColumn(getColumnSort());
|
||||
}
|
||||
|
||||
protected void setSortColumn(WalletTable.Sort sort) {
|
||||
if(sort.sortColumn() >= 0 && sort.sortColumn() < getColumns().size() && getSortOrder().isEmpty() && !getRoot().getChildren().isEmpty()) {
|
||||
TreeTableColumn<Entry, ?> column = getColumns().get(sort.sortColumn());
|
||||
column.setSortType(sort.sortDirection() == SortDirection.DESCENDING ? TreeTableColumn.SortType.DESCENDING : TreeTableColumn.SortType.ASCENDING);
|
||||
getSortOrder().add(column);
|
||||
}
|
||||
}
|
||||
|
||||
private WalletTable.Sort getColumnSort() {
|
||||
if(getSortOrder().isEmpty() || !getColumns().contains(getSortOrder().getFirst())) {
|
||||
return new WalletTable.Sort(tableType == TableType.UTXOS ? getColumns().size() - 1 : 0, SortDirection.DESCENDING);
|
||||
}
|
||||
|
||||
return new WalletTable.Sort(getColumns().indexOf(getSortOrder().getFirst()), getSortDirection(getSortOrder().getFirst().getSortType()));
|
||||
}
|
||||
|
||||
private SortDirection getSortDirection(TreeTableColumn.SortType sortType) {
|
||||
return sortType == TreeTableColumn.SortType.ASCENDING ? SortDirection.ASCENDING : SortDirection.DESCENDING;
|
||||
}
|
||||
|
||||
private WalletTable.Sort getSavedColumnSort() {
|
||||
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
|
||||
Wallet wallet = getRoot().getValue().getWallet();
|
||||
WalletTable walletTable = wallet.getWalletTable(tableType);
|
||||
if(walletTable != null) {
|
||||
return walletTable.getSort();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void setupColumnWidths() {
|
||||
Double[] savedWidths = getSavedColumnWidths();
|
||||
for(int i = 0; i < getColumns().size(); i++) {
|
||||
TreeTableColumn<Entry, ?> column = getColumns().get(i);
|
||||
column.setPrefWidth(savedWidths != null && getColumns().size() == savedWidths.length ? savedWidths[i] : STANDARD_WIDTH);
|
||||
}
|
||||
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
|
||||
|
||||
getColumns().getLast().widthProperty().addListener((_, _, _) -> walletTableChanged());
|
||||
|
||||
//Ignore initial resizes during layout
|
||||
walletTableEvents.skip(3, TimeUnit.SECONDS).subscribe(event -> {
|
||||
event.getWallet().getWalletTables().put(event.getTableType(), event.getWalletTable());
|
||||
EventManager.get().post(event);
|
||||
|
||||
//Reset pref widths here so window resizes don't cause reversion to previously set pref widths
|
||||
Double[] widths = event.getWalletTable().getWidths();
|
||||
for(int i = 0; i < getColumns().size(); i++) {
|
||||
TreeTableColumn<Entry, ?> column = getColumns().get(i);
|
||||
column.setPrefWidth(widths != null && getColumns().size() == widths.length ? widths[i] : STANDARD_WIDTH);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void walletTableChanged() {
|
||||
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
|
||||
WalletTable walletTable = new WalletTable(tableType, getColumnWidths(), getColumnSort());
|
||||
walletTableSubject.onNext(new WalletTableChangedEvent(getRoot().getValue().getWallet(), walletTable));
|
||||
}
|
||||
}
|
||||
|
||||
private Double[] getColumnWidths() {
|
||||
return getColumns().stream().map(TableColumnBase::getWidth).toArray(Double[]::new);
|
||||
}
|
||||
|
||||
private Double[] getSavedColumnWidths() {
|
||||
if(getRoot() != null && getRoot().getValue() != null && getRoot().getValue().getWallet() != null) {
|
||||
Wallet wallet = getRoot().getValue().getWallet();
|
||||
WalletTable walletTable = wallet.getWalletTable(tableType);
|
||||
if(walletTable != null) {
|
||||
return walletTable.getWidths();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,16 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.SeparatorMenuItem;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.controlsfx.control.textfield.CustomTextField;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ComboBoxTextField extends CustomTextField {
|
||||
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
|
||||
|
||||
@@ -68,4 +74,53 @@ public class ComboBoxTextField extends CustomTextField {
|
||||
public void setComboProperty(ComboBox<?> comboProperty) {
|
||||
this.comboProperty.set(comboProperty);
|
||||
}
|
||||
|
||||
public ContextMenu getCustomContextMenu(List<MenuItem> customItems) {
|
||||
return new CustomContextMenu(customItems);
|
||||
}
|
||||
|
||||
public class CustomContextMenu extends ContextMenu {
|
||||
public CustomContextMenu(List<MenuItem> customItems) {
|
||||
super();
|
||||
setFont(null);
|
||||
|
||||
MenuItem undo = new MenuItem("Undo");
|
||||
undo.setOnAction(_ -> undo());
|
||||
|
||||
MenuItem redo = new MenuItem("Redo");
|
||||
redo.setOnAction(_ -> redo());
|
||||
|
||||
MenuItem cut = new MenuItem("Cut");
|
||||
cut.setOnAction(_ -> cut());
|
||||
|
||||
MenuItem copy = new MenuItem("Copy");
|
||||
copy.setOnAction(_ -> copy());
|
||||
|
||||
MenuItem paste = new MenuItem("Paste");
|
||||
paste.setOnAction(_ -> paste());
|
||||
|
||||
MenuItem delete = new MenuItem("Delete");
|
||||
delete.setOnAction(_ -> deleteText(getSelection()));
|
||||
|
||||
MenuItem selectAll = new MenuItem("Select All");
|
||||
selectAll.setOnAction(_ -> selectAll());
|
||||
|
||||
getItems().addAll(undo, redo, new SeparatorMenuItem(), cut, copy, paste, delete, new SeparatorMenuItem(), selectAll);
|
||||
getItems().addAll(customItems);
|
||||
|
||||
setOnShowing(_ -> {
|
||||
boolean hasSelection = getSelection().getLength() > 0;
|
||||
boolean hasText = getText() != null && !getText().isEmpty();
|
||||
boolean clipboardHasContent = Clipboard.getSystemClipboard().hasString();
|
||||
|
||||
undo.setDisable(!isUndoable());
|
||||
redo.setDisable(!isRedoable());
|
||||
cut.setDisable(!isEditable() || !hasSelection);
|
||||
copy.setDisable(!hasSelection);
|
||||
paste.setDisable(!isEditable() || !clipboardHasContent);
|
||||
delete.setDisable(!hasSelection);
|
||||
selectAll.setDisable(!hasText);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.getActiveWindow;
|
||||
import static com.sparrowwallet.sparrow.AppServices.setStageIcon;
|
||||
|
||||
public class ConfirmationAlert extends Alert {
|
||||
private final CheckBox dontAskAgain;
|
||||
|
||||
public ConfirmationAlert(String title, String contentText, ButtonType... buttons) {
|
||||
super(AlertType.CONFIRMATION, contentText, buttons);
|
||||
|
||||
initOwner(getActiveWindow());
|
||||
setStageIcon(getDialogPane().getScene().getWindow());
|
||||
getDialogPane().getScene().getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
setTitle(title);
|
||||
setHeaderText(title);
|
||||
|
||||
VBox contentBox = new VBox(20);
|
||||
contentBox.setPadding(new Insets(10, 20, 10, 20));
|
||||
Label contentLabel = new Label(contentText);
|
||||
contentLabel.setWrapText(true);
|
||||
dontAskAgain = new CheckBox("Don't ask again");
|
||||
contentBox.getChildren().addAll(contentLabel, dontAskAgain);
|
||||
|
||||
getDialogPane().setContent(contentBox);
|
||||
}
|
||||
|
||||
public boolean isDontAskAgain() {
|
||||
return dontAskAgain.isSelected();
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,7 @@ import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
|
||||
public class CopyableCoinLabel extends CopyableLabel {
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
@@ -29,6 +28,10 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> setValueAsText((Long)newValue, Config.get().getUnitFormat(), Config.get().getBitcoinUnit()));
|
||||
|
||||
setOnMouseClicked(event -> {
|
||||
if(!event.getButton().equals(MouseButton.PRIMARY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(bitcoinUnit == null) {
|
||||
bitcoinUnit = Config.get().getBitcoinUnit();
|
||||
}
|
||||
@@ -67,6 +70,13 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
}
|
||||
|
||||
private void setValueAsText(Long value, UnitFormat unitFormat, BitcoinUnit bitcoinUnit) {
|
||||
if(Config.get().isHideAmounts()) {
|
||||
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
|
||||
setTooltip(null);
|
||||
setContextMenu(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ import org.fxmisc.richtext.CodeArea;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.sparrowwallet.drongo.policy.PolicyType.MULTI;
|
||||
import static com.sparrowwallet.drongo.policy.PolicyType.SINGLE;
|
||||
import static com.sparrowwallet.drongo.policy.PolicyType.*;
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.MULTISIG;
|
||||
|
||||
public class DescriptorArea extends CodeArea {
|
||||
@@ -33,13 +32,13 @@ public class DescriptorArea extends CodeArea {
|
||||
List<Keystore> keystores = wallet.getKeystores();
|
||||
int threshold = wallet.getDefaultPolicy().getNumSignaturesRequired();
|
||||
|
||||
if(SINGLE.equals(policyType)) {
|
||||
if(SINGLE_HD.equals(policyType)) {
|
||||
append(scriptType.getDescriptor(), "descriptor-text");
|
||||
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
|
||||
append(scriptType.getCloseDescriptor(), "descriptor-text");
|
||||
}
|
||||
|
||||
if(MULTI.equals(policyType)) {
|
||||
if(MULTI_HD.equals(policyType)) {
|
||||
append(scriptType.getDescriptor(), "descriptor-text");
|
||||
append(MULTISIG.getDescriptor(), "descriptor-text");
|
||||
append(Integer.toString(threshold), "descriptor-text");
|
||||
@@ -52,6 +51,12 @@ public class DescriptorArea extends CodeArea {
|
||||
append(MULTISIG.getCloseDescriptor(), "descriptor-text");
|
||||
append(scriptType.getCloseDescriptor(), "descriptor-text");
|
||||
}
|
||||
|
||||
if(SINGLE_SP.equals(policyType)) {
|
||||
append("sp(", "descriptor-text");
|
||||
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
|
||||
append(")", "descriptor-text");
|
||||
}
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
|
||||
@@ -9,8 +9,8 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.control.Button;
|
||||
|
||||
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, boolean selectBbqrButton) {
|
||||
super(ur, bbqr, false, false, selectBbqrButton);
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, QREncoding encoding) {
|
||||
super(ur, bbqr, false, false, encoding);
|
||||
|
||||
DialogPane dialogPane = getDialogPane();
|
||||
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
|
||||
@@ -20,8 +20,13 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
pdfButton.setGraphicTextGap(5);
|
||||
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
||||
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, isUseBbqrEncoding() ? bbqr : null);
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, getEncoding() == QREncoding.BBQR ? bbqr : null);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
ButtonBar buttonBar = (ButtonBar)dialogPane.lookup(".button-bar");
|
||||
if(buttonBar != null) {
|
||||
buttonBar.setButtonOrder("E+L+B+C+O");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,16 +12,15 @@ import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.CardApi;
|
||||
import com.sparrowwallet.sparrow.io.Device;
|
||||
import com.sparrowwallet.sparrow.io.Hwi;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.CardAuthorizationException;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
@@ -54,7 +53,8 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
private final Wallet wallet;
|
||||
private final PSBT psbt;
|
||||
private final OutputDescriptor outputDescriptor;
|
||||
private final KeyDerivation keyDerivation;
|
||||
private final KeyDerivation defaultDerivation;
|
||||
private final KeyDerivation requiredDerivation;
|
||||
private final String message;
|
||||
private final List<StandardAccount> availableAccounts;
|
||||
private final Device device;
|
||||
@@ -77,13 +77,14 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
private boolean defaultDevice;
|
||||
|
||||
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation requiredDerivation) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
public DevicePane(Wallet wallet, Device device, boolean defaultDevice, KeyDerivation defaultDerivation, KeyDerivation requiredDerivation) {
|
||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||
this.deviceOperation = DeviceOperation.IMPORT;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = requiredDerivation;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
this.message = null;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
@@ -105,12 +106,13 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, PSBT psbt, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||
this.deviceOperation = DeviceOperation.SIGN;
|
||||
this.wallet = wallet;
|
||||
this.psbt = psbt;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.defaultDerivation = null;
|
||||
this.requiredDerivation = null;
|
||||
this.message = null;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
@@ -132,12 +134,13 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, OutputDescriptor outputDescriptor, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||
this.deviceOperation = DeviceOperation.DISPLAY_ADDRESS;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = outputDescriptor;
|
||||
this.keyDerivation = null;
|
||||
this.defaultDerivation = null;
|
||||
this.requiredDerivation = null;
|
||||
this.message = null;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
@@ -154,13 +157,14 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
buttonBox.getChildren().addAll(setPassphraseButton, displayAddressButton);
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, String message, KeyDerivation keyDerivation, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
public DevicePane(Wallet wallet, String message, KeyDerivation requiredDerivation, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||
this.deviceOperation = DeviceOperation.SIGN_MESSAGE;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = keyDerivation;
|
||||
this.defaultDerivation = requiredDerivation;
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
this.message = message;
|
||||
this.availableAccounts = null;
|
||||
this.device = device;
|
||||
@@ -182,12 +186,13 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
|
||||
this.wallet = wallet;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.defaultDerivation = null;
|
||||
this.requiredDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
@@ -205,12 +210,13 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) {
|
||||
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
|
||||
super(device.getModel().toDisplayString(), "", "", device.getModel());
|
||||
this.deviceOperation = deviceOperation;
|
||||
this.wallet = null;
|
||||
this.psbt = null;
|
||||
this.outputDescriptor = null;
|
||||
this.keyDerivation = null;
|
||||
this.defaultDerivation = null;
|
||||
this.requiredDerivation = null;
|
||||
this.message = null;
|
||||
this.device = device;
|
||||
this.defaultDevice = defaultDevice;
|
||||
@@ -289,27 +295,50 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void createImportButton() {
|
||||
importButton = keyDerivation == null ? new SplitMenuButton() : new Button();
|
||||
importButton = requiredDerivation == null ? new SplitMenuButton() : new Button();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
importKeystore(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation());
|
||||
importKeystore(requiredDerivation == null ? getDefaultDerivation() : requiredDerivation.getDerivation());
|
||||
});
|
||||
|
||||
if(importButton instanceof SplitMenuButton importMenuButton) {
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType[] scriptTypes = new ScriptType[] {ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH, ScriptType.P2TR};
|
||||
for(ScriptType scriptType : scriptTypes) {
|
||||
MenuItem item = new MenuItem(scriptType.getDescription());
|
||||
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
|
||||
item.setOnAction(event -> {
|
||||
importMenuButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importMenuButton.getItems().add(item);
|
||||
if(wallet.getPolicyType() == null) {
|
||||
List<PolicyAndScriptType> types = new ArrayList<>();
|
||||
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
|
||||
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
|
||||
types.add(new PolicyAndScriptType(policyType, scriptType));
|
||||
}
|
||||
}
|
||||
for(PolicyAndScriptType type : types) {
|
||||
MenuItem item = new MenuItem(type.getDescription());
|
||||
final List<ChildNumber> derivation = type.scriptType().getDefaultDerivation();
|
||||
item.setOnAction(event -> {
|
||||
importMenuButton.setDisable(true);
|
||||
wallet.setPolicyType(type.policyType());
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importMenuButton.getItems().add(item);
|
||||
}
|
||||
} else {
|
||||
List<ScriptType> scriptTypes = ScriptType.getScriptTypesForPolicyType(wallet.getPolicyType());
|
||||
for(ScriptType scriptType : scriptTypes) {
|
||||
MenuItem item = new MenuItem(scriptType.getDescription());
|
||||
final List<ChildNumber> derivation = scriptType.getDefaultDerivation();
|
||||
item.setOnAction(event -> {
|
||||
importMenuButton.setDisable(true);
|
||||
importKeystore(derivation);
|
||||
});
|
||||
importMenuButton.getItems().add(item);
|
||||
}
|
||||
}
|
||||
importMenuButton.getItems().add(new SeparatorMenuItem());
|
||||
MenuItem discoverItem = new MenuItem("Discover Wallet...");
|
||||
discoverItem.setDisable(!AppServices.isConnected());
|
||||
discoverItem.setOnAction(_ -> discoverWallet());
|
||||
importMenuButton.getItems().add(discoverItem);
|
||||
} else {
|
||||
String[] accounts = new String[] {"Default Account #0", "Account #1", "Account #2", "Account #3", "Account #4", "Account #5", "Account #6", "Account #7", "Account #8", "Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
@@ -366,7 +395,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
signMessageButton.managedProperty().bind(signMessageButton.visibleProperty());
|
||||
signMessageButton.setVisible(false);
|
||||
|
||||
if(device.getFingerprint() != null && !device.getFingerprint().equals(keyDerivation.getMasterFingerprint())) {
|
||||
if(device.getFingerprint() != null && !device.getFingerprint().equals(requiredDerivation.getMasterFingerprint())) {
|
||||
signMessageButton.setDisable(true);
|
||||
}
|
||||
}
|
||||
@@ -375,7 +404,6 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
discoverKeystoresButton = new Button("Discover");
|
||||
discoverKeystoresButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
discoverKeystoresButton.setOnAction(event -> {
|
||||
discoverKeystoresButton.setDisable(true);
|
||||
discoverKeystores();
|
||||
});
|
||||
discoverKeystoresButton.managedProperty().bind(discoverKeystoresButton.visibleProperty());
|
||||
@@ -436,6 +464,14 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
getAddressButton.setVisible(false);
|
||||
}
|
||||
|
||||
private List<ChildNumber> getDefaultDerivation() {
|
||||
if(defaultDerivation != null && !defaultDerivation.getDerivation().isEmpty()) {
|
||||
return defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
return wallet == null || wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
}
|
||||
|
||||
private void unlock(Device device) {
|
||||
if(device.getModel().requiresPinPrompt()) {
|
||||
promptPin();
|
||||
@@ -456,20 +492,26 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
});
|
||||
vBox.getChildren().addAll(pinField, enterPinButton);
|
||||
|
||||
TilePane tilePane = new TilePane();
|
||||
tilePane.setPrefColumns(3);
|
||||
tilePane.setHgap(10);
|
||||
tilePane.setVgap(10);
|
||||
tilePane.setMaxWidth(150);
|
||||
tilePane.setMaxHeight(120);
|
||||
GridPane gridPane = new GridPane();
|
||||
gridPane.setHgap(10);
|
||||
gridPane.setVgap(10);
|
||||
gridPane.setMaxWidth(150);
|
||||
gridPane.setMaxHeight(device.getModel().hasZeroInPin() ? 160 : 120);
|
||||
|
||||
int[] digits = new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
||||
int[] digits = device.getModel().hasZeroInPin() ? new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3, 0} : new int[] {7, 8, 9, 4, 5, 6, 1, 2, 3};
|
||||
for(int i = 0; i < digits.length; i++) {
|
||||
Button pinButton = new Button();
|
||||
Glyph circle = new Glyph(FontAwesome5.FONT_NAME, "CIRCLE");
|
||||
pinButton.setGraphic(circle);
|
||||
pinButton.setUserData(digits[i]);
|
||||
tilePane.getChildren().add(pinButton);
|
||||
GridPane.setRowIndex(pinButton, i / 3);
|
||||
GridPane.setColumnIndex(pinButton, i % 3);
|
||||
if((i / 3) == 3) {
|
||||
GridPane.setHgrow(pinButton, Priority.ALWAYS);
|
||||
GridPane.setColumnSpan(pinButton, 3);
|
||||
pinButton.setMaxWidth(Double.MAX_VALUE);
|
||||
}
|
||||
gridPane.getChildren().add(pinButton);
|
||||
pinButton.setOnAction(event -> {
|
||||
pinField.setText(pinField.getText() + pinButton.getUserData());
|
||||
});
|
||||
@@ -477,7 +519,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setSpacing(50);
|
||||
contentBox.getChildren().add(tilePane);
|
||||
contentBox.getChildren().add(gridPane);
|
||||
contentBox.getChildren().add(vBox);
|
||||
contentBox.setPadding(new Insets(10, 0, 10, 0));
|
||||
contentBox.setAlignment(Pos.TOP_CENTER);
|
||||
@@ -497,7 +539,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
SplitMenuButton sendPassphraseButton = new SplitMenuButton();
|
||||
sendPassphraseButton.setText("Send Passphrase");
|
||||
sendPassphraseButton.getStyleClass().add("default-button");
|
||||
setDefaultButton(sendPassphraseButton);
|
||||
sendPassphraseButton.setOnAction(event -> {
|
||||
setExpanded(false);
|
||||
setDescription("Confirm passphrase on device...");
|
||||
@@ -689,7 +731,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
return;
|
||||
}
|
||||
|
||||
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
|
||||
Service<Keystore> importService = cardApi.getImportService(wallet.getPolicyType(), derivation, messageProperty);
|
||||
handleCardOperation(importService, importButton, "Import", true, event -> {
|
||||
importKeystore(derivation, importService.getValue());
|
||||
});
|
||||
@@ -708,13 +750,21 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
importXpub(derivation);
|
||||
importKey(derivation);
|
||||
});
|
||||
enumerateService.setOnFailed(workerStateEvent -> {
|
||||
setError("Error", enumerateService.getException().getMessage());
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
enumerateService.start();
|
||||
} else {
|
||||
importKey(derivation);
|
||||
}
|
||||
}
|
||||
|
||||
private void importKey(List<ChildNumber> derivation) {
|
||||
if(wallet != null && wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
importSpscan(derivation);
|
||||
} else {
|
||||
importXpub(derivation);
|
||||
}
|
||||
@@ -725,7 +775,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
|
||||
Hwi.GetXpubService getXpubService = new Hwi.GetXpubService(device, passphrase.get(), derivationPath);
|
||||
getXpubService.setOnSucceeded(workerStateEvent -> {
|
||||
String xpub = getXpubService.getValue();
|
||||
ExtendedKey xpub = getXpubService.getValue();
|
||||
|
||||
try {
|
||||
Keystore keystore = new Keystore();
|
||||
@@ -733,7 +783,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(xpub));
|
||||
keystore.setExtendedPublicKey(xpub);
|
||||
|
||||
importKeystore(derivation, keystore);
|
||||
} catch(Exception e) {
|
||||
@@ -749,14 +799,44 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
getXpubService.start();
|
||||
}
|
||||
|
||||
private void importSpscan(List<ChildNumber> derivation) {
|
||||
String derivationPath = KeyDerivation.writePath(derivation);
|
||||
|
||||
Hwi.GetSpscanService getSpscanService = new Hwi.GetSpscanService(device, passphrase.get(), derivationPath);
|
||||
getSpscanService.setOnSucceeded(workerStateEvent -> {
|
||||
SilentPaymentScanAddress spscan = getSpscanService.getValue();
|
||||
|
||||
try {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPath));
|
||||
keystore.setSilentPaymentScanAddress(spscan);
|
||||
|
||||
importKeystore(derivation, keystore);
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve spscan", e.getMessage());
|
||||
}
|
||||
});
|
||||
getSpscanService.setOnFailed(workerStateEvent -> {
|
||||
setError("Could not retrieve spscan", getSpscanService.getException().getMessage());
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
setDescription("Importing...");
|
||||
showHideLink.setVisible(false);
|
||||
getSpscanService.start();
|
||||
}
|
||||
|
||||
private void importKeystore(List<ChildNumber> derivation, Keystore keystore) {
|
||||
if(wallet.getScriptType() == null) {
|
||||
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().get(0).equals(derivation.get(0))).findFirst().orElse(ScriptType.P2PKH);
|
||||
ScriptType scriptType = Arrays.stream(ScriptType.ADDRESSABLE_TYPES).filter(type -> type.getDefaultDerivation().getFirst().equals(derivation.getFirst())).findFirst().orElse(ScriptType.P2PKH);
|
||||
PolicyType policyType = wallet.getPolicyType() != null ? wallet.getPolicyType() : PolicyType.SINGLE_HD;
|
||||
wallet.setName(device.getModel().toDisplayString());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setPolicyType(policyType);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
|
||||
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
@@ -778,10 +858,12 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
signButton.setDisable(false);
|
||||
}
|
||||
} else {
|
||||
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt);
|
||||
Hwi.SignPSBTService signPSBTService = new Hwi.SignPSBTService(device, passphrase.get(), psbt,
|
||||
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
|
||||
signPSBTService.setOnSucceeded(workerStateEvent -> {
|
||||
PSBT signedPsbt = signPSBTService.getValue();
|
||||
EventManager.get().post(new PSBTSignedEvent(psbt, signedPsbt));
|
||||
updateDeviceRegistrations(signPSBTService.getNewDeviceRegistrations());
|
||||
});
|
||||
signPSBTService.setOnFailed(workerStateEvent -> {
|
||||
setError("Signing Error", signPSBTService.getException().getMessage());
|
||||
@@ -820,10 +902,12 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
|
||||
private void displayAddress() {
|
||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor);
|
||||
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(device, passphrase.get(), wallet.getScriptType(), outputDescriptor,
|
||||
OutputDescriptor.getOutputDescriptor(wallet), wallet.getFullName(), getDeviceRegistration());
|
||||
displayAddressService.setOnSucceeded(successEvent -> {
|
||||
String address = displayAddressService.getValue();
|
||||
EventManager.get().post(new AddressDisplayedEvent(address));
|
||||
updateDeviceRegistrations(displayAddressService.getNewDeviceRegistrations());
|
||||
});
|
||||
displayAddressService.setOnFailed(failedEvent -> {
|
||||
setError("Could not display address", displayAddressService.getException().getMessage());
|
||||
@@ -833,11 +917,31 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
displayAddressService.start();
|
||||
}
|
||||
|
||||
private byte[] getDeviceRegistration() {
|
||||
Optional<Keystore> optKeystore = wallet.getKeystores().stream()
|
||||
.filter(keystore -> keystore.getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint()) && keystore.getDeviceRegistration() != null).findFirst();
|
||||
return optKeystore.map(Keystore::getDeviceRegistration).orElse(null);
|
||||
}
|
||||
|
||||
private void updateDeviceRegistrations(Set<byte[]> newDeviceRegistrations) {
|
||||
if(!newDeviceRegistrations.isEmpty()) {
|
||||
List<Keystore> registrationKeystores = getDeviceRegistrationKeystores();
|
||||
if(!registrationKeystores.isEmpty()) {
|
||||
registrationKeystores.forEach(keystore -> keystore.setDeviceRegistration(newDeviceRegistrations.iterator().next()));
|
||||
EventManager.get().post(new KeystoreDeviceRegistrationsChangedEvent(wallet, registrationKeystores));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<Keystore> getDeviceRegistrationKeystores() {
|
||||
return wallet.getKeystores().stream().filter(keystore -> keystore.getKeyDerivation().getMasterFingerprint().equals(device.getFingerprint())).toList();
|
||||
}
|
||||
|
||||
private void signMessage() {
|
||||
if(device.isCard()) {
|
||||
try {
|
||||
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
|
||||
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty);
|
||||
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), requiredDerivation.getDerivation(), messageProperty);
|
||||
handleCardOperation(signMessageService, signMessageButton, "Signing", true, event -> {
|
||||
String signature = signMessageService.getValue();
|
||||
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||
@@ -848,7 +952,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
signButton.setDisable(false);
|
||||
}
|
||||
} else {
|
||||
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath());
|
||||
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, requiredDerivation.getDerivationPath());
|
||||
signMessageService.setOnSucceeded(successEvent -> {
|
||||
String signature = signMessageService.getValue();
|
||||
EventManager.get().post(new MessageSignedEvent(wallet, signature));
|
||||
@@ -862,37 +966,141 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
private void discoverWallet() {
|
||||
importButton.setDisable(true);
|
||||
importButton.setMaxHeight(importButton.getHeight());
|
||||
ProgressIndicator progressIndicator = new ProgressIndicator(0);
|
||||
progressIndicator.getStyleClass().add("button-progress");
|
||||
importButton.setGraphic(progressIndicator);
|
||||
List<Wallet> wallets = new ArrayList<>();
|
||||
|
||||
RangeInputDialog rangeInputDialog = new RangeInputDialog(StandardAccount.ACCOUNT_0.getAccountNumber(), StandardAccount.ACCOUNT_30.getAccountNumber(), StandardAccount.ACCOUNT_10.getAccountNumber());
|
||||
rangeInputDialog.setTitle("Choose number of accounts");
|
||||
rangeInputDialog.setHeaderText("Enter the number of additional accounts to scan for existing funds.\n\nThis may take a few minutes depending on how many accounts are selected.");
|
||||
Optional<Integer> optRange = rangeInputDialog.showAndWait();
|
||||
if(optRange.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<StandardAccount> discoveryAccounts = new ArrayList<>(Arrays.asList(StandardAccount.values()).subList(0, optRange.get() + 1));
|
||||
Map<Hwi.WalletType, String> derivationPaths = new LinkedHashMap<>();
|
||||
List<ScriptType> scriptTypes = new ArrayList<>(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD));
|
||||
if(device.getModel() == WalletModel.BITBOX_02) {
|
||||
scriptTypes.remove(ScriptType.P2PKH);
|
||||
}
|
||||
for(ScriptType scriptType : scriptTypes) {
|
||||
for(StandardAccount discoveryAccount : discoveryAccounts) {
|
||||
derivationPaths.put(new Hwi.WalletType(scriptType, discoveryAccount), KeyDerivation.writePath(scriptType.getDefaultDerivation(discoveryAccount.getAccountNumber())));
|
||||
}
|
||||
}
|
||||
|
||||
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), derivationPaths);
|
||||
getXpubsService.setOnSucceeded(_ -> {
|
||||
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
|
||||
|
||||
for(Map.Entry<Hwi.WalletType, ExtendedKey> entry : accountXpubs.entrySet()) {
|
||||
try {
|
||||
Wallet wallet = new Wallet(device.getModel().toDisplayString());
|
||||
wallet.setPolicyType(PolicyType.SINGLE_HD);
|
||||
wallet.setScriptType(entry.getKey().scriptType());
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(device.getFingerprint(), derivationPaths.get(entry.getKey())));
|
||||
keystore.setExtendedPublicKey(entry.getValue());
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE_HD, entry.getKey().scriptType(), wallet.getKeystores(), 1));
|
||||
if(entry.getKey().standardAccount().equals(StandardAccount.ACCOUNT_0)) {
|
||||
wallets.add(wallet);
|
||||
} else {
|
||||
Wallet masterWallet = wallets.getLast();
|
||||
wallet.setName(entry.getKey().standardAccount().getName());
|
||||
wallet.setMasterWallet(masterWallet);
|
||||
masterWallet.getChildWallets().add(wallet);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve xpub", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallets);
|
||||
walletDiscoveryService.setOnSucceeded(_ -> {
|
||||
importButton.setGraphic(null);
|
||||
Optional<List<Wallet>> optWallets = walletDiscoveryService.getValue();
|
||||
if(optWallets.isPresent()) {
|
||||
List<Wallet> discoveredWallets = optWallets.get();
|
||||
if(discoveredWallets.size() > 1) {
|
||||
for(Wallet wallet : discoveredWallets) {
|
||||
wallet.setName(wallet.getName() + " " + wallet.getScriptType().getDescription());
|
||||
}
|
||||
}
|
||||
EventManager.get().post(new WalletImportEvent(discoveredWallets));
|
||||
} else {
|
||||
AppServices.showErrorDialog("No existing wallet found",
|
||||
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
|
||||
"You can however import the " + device.getModel().toDisplayString() + " and scan the blockchain by supplying a start date." :
|
||||
"Could not find an HD wallet with existing transactions using the " + device.getModel().toDisplayString() + ".");
|
||||
setDefaultStatus();
|
||||
importButton.setDisable(false);
|
||||
}
|
||||
});
|
||||
walletDiscoveryService.setOnFailed(failedEvent -> {
|
||||
log.error("Failed to discover wallets", failedEvent.getSource().getException());
|
||||
setError("Failed to discover wallets", failedEvent.getSource().getException().getMessage());
|
||||
importButton.setGraphic(null);
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
walletDiscoveryService.start();
|
||||
});
|
||||
getXpubsService.setOnFailed(_ -> {
|
||||
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
|
||||
importButton.setGraphic(null);
|
||||
importButton.setDisable(false);
|
||||
});
|
||||
progressIndicator.progressProperty().bind(getXpubsService.progressProperty());
|
||||
getXpubsService.progressProperty().addListener((_, _, newValue) -> setDescription("Discovering... (" + Math.round(newValue.doubleValue() * 100) + "%)"));
|
||||
showHideLink.setVisible(false);
|
||||
getXpubsService.start();
|
||||
}
|
||||
|
||||
private void discoverKeystores() {
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
setError("Could not discover keystores", "Only single signature wallets are supported for keystore discovery");
|
||||
return;
|
||||
}
|
||||
|
||||
discoverKeystoresButton.setDisable(true);
|
||||
discoverKeystoresButton.setMaxHeight(discoverKeystoresButton.getHeight());
|
||||
ProgressIndicator progressIndicator = new ProgressIndicator(0);
|
||||
progressIndicator.getStyleClass().add("button-progress");
|
||||
discoverKeystoresButton.setGraphic(progressIndicator);
|
||||
|
||||
String masterFingerprint = wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
|
||||
|
||||
Wallet copyWallet = wallet.copy();
|
||||
Map<StandardAccount, String> accountDerivationPaths = new LinkedHashMap<>();
|
||||
Map<Hwi.WalletType, String> accountDerivationPaths = new LinkedHashMap<>();
|
||||
for(StandardAccount availableAccount : availableAccounts) {
|
||||
Wallet availableWallet = copyWallet.addChildWallet(availableAccount);
|
||||
Keystore availableKeystore = availableWallet.getKeystores().get(0);
|
||||
String derivationPath = availableKeystore.getKeyDerivation().getDerivationPath();
|
||||
accountDerivationPaths.put(availableAccount, derivationPath);
|
||||
accountDerivationPaths.put(new Hwi.WalletType(wallet.getScriptType(), availableAccount), derivationPath);
|
||||
}
|
||||
|
||||
Map<StandardAccount, Keystore> importedKeystores = new LinkedHashMap<>();
|
||||
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), accountDerivationPaths);
|
||||
getXpubsService.setOnSucceeded(workerStateEvent -> {
|
||||
Map<StandardAccount, String> accountXpubs = getXpubsService.getValue();
|
||||
Map<Hwi.WalletType, ExtendedKey> accountXpubs = getXpubsService.getValue();
|
||||
|
||||
for(Map.Entry<StandardAccount, String> entry : accountXpubs.entrySet()) {
|
||||
for(Map.Entry<Hwi.WalletType, ExtendedKey> entry : accountXpubs.entrySet()) {
|
||||
try {
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(device.getModel().toDisplayString());
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
keystore.setWalletModel(device.getModel());
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, accountDerivationPaths.get(entry.getKey())));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
|
||||
importedKeystores.put(entry.getKey(), keystore);
|
||||
keystore.setExtendedPublicKey(entry.getValue());
|
||||
importedKeystores.put(entry.getKey().standardAccount(), keystore);
|
||||
} catch(Exception e) {
|
||||
setError("Could not retrieve xpub", e.getMessage());
|
||||
}
|
||||
@@ -906,15 +1114,18 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
accountDiscoveryService.setOnFailed(event -> {
|
||||
log.error("Failed to discover accounts", event.getSource().getException());
|
||||
setError("Failed to discover accounts", event.getSource().getException().getMessage());
|
||||
discoverKeystoresButton.setGraphic(null);
|
||||
discoverKeystoresButton.setDisable(false);
|
||||
});
|
||||
accountDiscoveryService.start();
|
||||
});
|
||||
getXpubsService.setOnFailed(workerStateEvent -> {
|
||||
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
|
||||
discoverKeystoresButton.setGraphic(null);
|
||||
discoverKeystoresButton.setDisable(false);
|
||||
});
|
||||
setDescription("Discovering...");
|
||||
progressIndicator.progressProperty().bind(getXpubsService.progressProperty());
|
||||
getXpubsService.progressProperty().addListener((_, _, newValue) -> setDescription("Discovering... (" + Math.round(newValue.doubleValue() * 100) + "%)"));
|
||||
showHideLink.setVisible(false);
|
||||
getXpubsService.start();
|
||||
}
|
||||
@@ -971,13 +1182,12 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
private void showOperationButton() {
|
||||
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
|
||||
if(defaultDevice) {
|
||||
importButton.getStyleClass().add("default-button");
|
||||
setDefaultButton(importButton);
|
||||
}
|
||||
importButton.setVisible(true);
|
||||
showHideLink.setText("Show derivation...");
|
||||
showHideLink.setVisible(!device.isCard());
|
||||
List<ChildNumber> defaultDerivation = wallet.getScriptType() == null ? ScriptType.P2WPKH.getDefaultDerivation() : wallet.getScriptType().getDefaultDerivation();
|
||||
setContent(getDerivationEntry(keyDerivation == null ? defaultDerivation : keyDerivation.getDerivation()));
|
||||
setContent(getDerivationEntry(requiredDerivation == null ? getDefaultDerivation() : requiredDerivation.getDerivation()));
|
||||
} else if(deviceOperation.equals(DeviceOperation.SIGN)) {
|
||||
signButton.setDefaultButton(defaultDevice);
|
||||
signButton.setVisible(true);
|
||||
@@ -996,7 +1206,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
showHideLink.setVisible(false);
|
||||
} else if(deviceOperation.equals(DeviceOperation.GET_PRIVATE_KEY)) {
|
||||
if(defaultDevice) {
|
||||
getPrivateKeyButton.getStyleClass().add("default-button");
|
||||
setDefaultButton(getPrivateKeyButton);
|
||||
}
|
||||
getPrivateKeyButton.setVisible(true);
|
||||
showHideLink.setVisible(false);
|
||||
@@ -1011,7 +1221,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||
derivationField.setDisable(device.isCard() || keyDerivation != null);
|
||||
derivationField.setDisable(device.isCard() || requiredDerivation != null);
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
@@ -1027,7 +1237,7 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
|
||||
importXpub(importDerivation);
|
||||
importKey(importDerivation);
|
||||
});
|
||||
|
||||
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
@@ -1203,4 +1413,10 @@ public class DevicePane extends TitledDescriptionPane {
|
||||
public enum DeviceOperation {
|
||||
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, GET_PRIVATE_KEY, GET_ADDRESS;
|
||||
}
|
||||
|
||||
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
|
||||
public String getDescription() {
|
||||
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
|
||||
public class DialogImage extends StackPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(DialogImage.class);
|
||||
|
||||
public static final int WIDTH = 50;
|
||||
public static final int HEIGHT = 50;
|
||||
|
||||
public ObjectProperty<DialogImage.Type> typeProperty = new SimpleObjectProperty<>();
|
||||
|
||||
public DialogImage() {
|
||||
getStyleClass().add("dialog-image");
|
||||
setPrefSize(WIDTH, HEIGHT);
|
||||
this.typeProperty.addListener((observable, oldValue, type) -> {
|
||||
refresh(type);
|
||||
});
|
||||
}
|
||||
|
||||
public DialogImage(@NamedArg("type") Type type) {
|
||||
this();
|
||||
this.typeProperty.set(type);
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
Type type = getType();
|
||||
refresh(type);
|
||||
}
|
||||
|
||||
protected void refresh(Type type) {
|
||||
SVGImage svgImage;
|
||||
if(Config.get().getTheme() == Theme.DARK) {
|
||||
svgImage = loadSVGImage("/image/dialog/" + type.name().toLowerCase(Locale.ROOT) + "-invert.svg");
|
||||
} else {
|
||||
svgImage = loadSVGImage("/image/dialog/" + type.name().toLowerCase(Locale.ROOT) + ".svg");
|
||||
}
|
||||
|
||||
if(svgImage != null) {
|
||||
getChildren().clear();
|
||||
getChildren().add(svgImage);
|
||||
}
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return typeProperty.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Type> typeProperty() {
|
||||
return typeProperty;
|
||||
}
|
||||
|
||||
public void setType(Type type) {
|
||||
this.typeProperty.set(type);
|
||||
}
|
||||
|
||||
private SVGImage loadSVGImage(String imageName) {
|
||||
try {
|
||||
URL url = AppServices.class.getResource(imageName);
|
||||
if(url != null) {
|
||||
return SVGLoader.load(url);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Could not find image " + imageName);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
SPARROW, SEED, PAYNYM, BORDERWALLETS, USERADD, WHIRLPOOL;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.pgp.PGPKeySource;
|
||||
import com.sparrowwallet.drongo.pgp.PGPUtils;
|
||||
@@ -24,7 +25,6 @@ import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
@@ -56,13 +56,15 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private static final List<String> MANIFEST_EXTENSIONS = List.of("txt");
|
||||
private static final List<String> PUBLIC_KEY_EXTENSIONS = List.of("asc");
|
||||
private static final List<String> MACOS_RELEASE_EXTENSIONS = List.of("dmg");
|
||||
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "zip");
|
||||
private static final List<String> WINDOWS_RELEASE_EXTENSIONS = List.of("exe", "msi", "zip");
|
||||
private static final List<String> LINUX_RELEASE_EXTENSIONS = List.of("deb", "rpm", "tar.gz");
|
||||
private static final List<String> DISK_IMAGE_EXTENSIONS = List.of("img", "bin", "dfu");
|
||||
private static final List<String> ARCHIVE_EXTENSIONS = List.of("zip", "tar.gz", "tar.bz2", "tar.xz", "rar", "7z");
|
||||
|
||||
private static final String SPARROW_RELEASE_PREFIX = "sparrow-";
|
||||
private static final String SPARROW_SIGNATURE_SUFFIX = "-manifest.txt.asc";
|
||||
private static final String[] SPARROW_RELEASE_ALT_PREFIXES = { "sparrowwallet-", "sparrowwallet_", "sparrowserver-", "sparrowserver_" };
|
||||
private static final String SPARROW_MANIFEST_SUFFIX = "-manifest.txt";
|
||||
private static final String SPARROW_SIGNATURE_SUFFIX = SPARROW_MANIFEST_SUFFIX + ".asc";
|
||||
private static final Pattern SPARROW_RELEASE_VERSION = Pattern.compile("[0-9]+(\\.[0-9]+)*");
|
||||
private static final long MIN_VALID_SPARROW_RELEASE_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
@@ -70,6 +72,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private final ObjectProperty<File> manifest = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<File> publicKey = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<File> release = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<File> initial = new SimpleObjectProperty<>();
|
||||
|
||||
private final BooleanProperty manifestDisabled = new SimpleBooleanProperty();
|
||||
private final BooleanProperty publicKeyDisabled = new SimpleBooleanProperty();
|
||||
@@ -81,7 +84,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
private static File lastFileParent;
|
||||
|
||||
public DownloadVerifierDialog(File initialSignatureFile) {
|
||||
public DownloadVerifierDialog(File initialFile) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
@@ -223,11 +226,17 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
});
|
||||
|
||||
release.addListener((observable, oldValue, releaseFile) -> {
|
||||
if(releaseFile != null) {
|
||||
initial.set(null);
|
||||
}
|
||||
verify();
|
||||
});
|
||||
|
||||
if(initialSignatureFile != null) {
|
||||
javafx.application.Platform.runLater(() -> signature.set(initialSignatureFile));
|
||||
if(initialFile != null) {
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
initial.set(initialFile);
|
||||
signature.set(initialFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +301,7 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
publicKeyDisabled.set(true);
|
||||
}
|
||||
|
||||
if(manifest.get().equals(release.get())) {
|
||||
if(manifest.get().equals(release.get()) && !isSparrowManifest(manifest.get())) {
|
||||
manifestDisabled.set(true);
|
||||
releaseHash.setText("No hash required, signature signs release file directly");
|
||||
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
|
||||
@@ -455,7 +464,8 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
if(providedFile.getName().toLowerCase(Locale.ROOT).startsWith(SPARROW_RELEASE_PREFIX)) {
|
||||
String providedName = providedFile.getName().toLowerCase(Locale.ROOT);
|
||||
if(providedName.startsWith(SPARROW_RELEASE_PREFIX) || Arrays.stream(SPARROW_RELEASE_ALT_PREFIXES).anyMatch(providedName::startsWith)) {
|
||||
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(providedFile.getName());
|
||||
if(matcher.find()) {
|
||||
String version = matcher.group();
|
||||
@@ -482,6 +492,22 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
private File findReleaseFile(File manifestFile, Map<File, String> manifestMap) {
|
||||
File initialFile = initial.get();
|
||||
if(initialFile != null && initialFile.exists()) {
|
||||
for(File file : manifestMap.keySet()) {
|
||||
if(initialFile.getName().equals(file.getName())) {
|
||||
return initialFile;
|
||||
}
|
||||
}
|
||||
|
||||
List<List<String>> allExtensionLists = List.of(MACOS_RELEASE_EXTENSIONS, WINDOWS_RELEASE_EXTENSIONS, LINUX_RELEASE_EXTENSIONS, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS);
|
||||
for(List<String> extensions : allExtensionLists) {
|
||||
if(extensions.stream().anyMatch(ext -> initialFile.getName().toLowerCase(Locale.ROOT).endsWith(ext))) {
|
||||
return initialFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<String> releaseExtensions = getReleaseFileExtensions();
|
||||
List<List<String>> extensionLists = List.of(releaseExtensions, DISK_IMAGE_EXTENSIONS, ARCHIVE_EXTENSIONS, List.of(""));
|
||||
|
||||
@@ -500,9 +526,9 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
private List<String> getReleaseFileExtensions() {
|
||||
Platform platform = Platform.getCurrent();
|
||||
switch(platform) {
|
||||
case OSX -> {
|
||||
OsType osType = OsType.getCurrent();
|
||||
switch(osType) {
|
||||
case MACOS -> {
|
||||
return MACOS_RELEASE_EXTENSIONS;
|
||||
}
|
||||
case WINDOWS -> {
|
||||
@@ -515,10 +541,10 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
private String getReleaseFileExample(String version) {
|
||||
Platform platform = Platform.getCurrent();
|
||||
OsType osType = OsType.getCurrent();
|
||||
String arch = System.getProperty("os.arch");
|
||||
switch(platform) {
|
||||
case OSX -> {
|
||||
switch(osType) {
|
||||
case MACOS -> {
|
||||
return "Sparrow-" + version + "-" + arch;
|
||||
}
|
||||
case WINDOWS -> {
|
||||
@@ -565,7 +591,8 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
if(name.startsWith(SPARROW_RELEASE_PREFIX) && file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
|
||||
if((name.startsWith(SPARROW_RELEASE_PREFIX) || Arrays.stream(SPARROW_RELEASE_ALT_PREFIXES).anyMatch(name::startsWith))
|
||||
&& file.length() >= MIN_VALID_SPARROW_RELEASE_SIZE) {
|
||||
Matcher matcher = SPARROW_RELEASE_VERSION.matcher(name);
|
||||
return matcher.find();
|
||||
}
|
||||
@@ -574,10 +601,18 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean isSparrowManifest(File manifestFile) {
|
||||
return manifestFile.getName().startsWith(SPARROW_RELEASE_PREFIX) && manifestFile.getName().endsWith(SPARROW_MANIFEST_SUFFIX);
|
||||
}
|
||||
|
||||
public void setSignatureFile(File signatureFile) {
|
||||
signature.set(signatureFile);
|
||||
}
|
||||
|
||||
public void setInitialFile(File initialFile) {
|
||||
initial.set(initialFile);
|
||||
}
|
||||
|
||||
private static class Header extends GridPane {
|
||||
public Header() {
|
||||
setMaxWidth(Double.MAX_VALUE);
|
||||
@@ -598,15 +633,8 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
vBox.getChildren().addAll(headerLabel, descriptionLabel);
|
||||
add(vBox, 0, 0);
|
||||
|
||||
StackPane graphicContainer = new StackPane();
|
||||
StackPane graphicContainer = new DialogImage(DialogImage.Type.SPARROW);
|
||||
graphicContainer.getStyleClass().add("graphic-container");
|
||||
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);
|
||||
graphicContainer.getChildren().add(imageView);
|
||||
}
|
||||
add(graphicContainer, 1, 0);
|
||||
|
||||
ColumnConstraints textColumn = new ColumnConstraints();
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -54,7 +58,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
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()) {
|
||||
if(this == lastCell && !getTableRow().isVisible() && isTableSizeRecalculation()) {
|
||||
return;
|
||||
}
|
||||
lastCell = this;
|
||||
@@ -65,8 +69,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
if(entry instanceof TransactionEntry) {
|
||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHeight() == -1) {
|
||||
setText("Unconfirmed Parent");
|
||||
setContextMenu(new UnconfirmedTransactionContextMenu(transactionEntry));
|
||||
@@ -100,7 +103,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
actionBox.getChildren().add(viewTransactionButton);
|
||||
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction) &&
|
||||
if(blockTransaction.getHeight() <= 0 && canRBF(blockTransaction, transactionEntry.getWallet()) &&
|
||||
Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
Button increaseFeeButton = new Button("");
|
||||
increaseFeeButton.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
@@ -120,21 +123,20 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
}
|
||||
|
||||
setGraphic(actionBox);
|
||||
} else if(entry instanceof NodeEntry) {
|
||||
NodeEntry nodeEntry = (NodeEntry)entry;
|
||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
||||
Address address = nodeEntry.getAddress();
|
||||
getStyleClass().add("address-cell");
|
||||
setText(address.toString());
|
||||
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry, true, getTreeTableView()));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(nodeEntry.getNode().toString());
|
||||
setTooltip(tooltip);
|
||||
getStyleClass().add("address-cell");
|
||||
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
|
||||
if(!nodeEntry.getNode().getWallet().isBip47()) {
|
||||
if(!nodeEntry.getNode().getWallet().isBip47() && nodeEntry.getNode().getWallet().getPolicyType() != PolicyType.SINGLE_SP) {
|
||||
Button receiveButton = new Button("");
|
||||
receiveButton.setGraphic(getReceiveGlyph());
|
||||
receiveButton.setOnAction(event -> {
|
||||
@@ -162,8 +164,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
setContextMenu(null);
|
||||
setGraphic(new HBox());
|
||||
}
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
setText(hashIndexEntry.getDescription());
|
||||
setContextMenu(getTreeTableView().getStyleClass().contains("bip47") ? null : new HashIndexEntryContextMenu(getTreeTableView(), hashIndexEntry));
|
||||
Tooltip tooltip = new Tooltip();
|
||||
@@ -211,13 +212,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
private static void increaseFee(TransactionEntry transactionEntry, boolean cancelTransaction) {
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
boolean silentPaymentTransaction = transactionEntry.getWallet().isSilentPaymentsTransaction(blockTransaction);
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = transactionEntry.getWallet().getWalletTxos();
|
||||
List<BlockTransactionHashIndex> utxos = transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.INPUT) && e.isSpendable())
|
||||
.map(e -> blockTransaction.getTransaction().getInputs().get((int)e.getHashIndex().getIndex()))
|
||||
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled())
|
||||
.filter(i -> Config.get().isMempoolFullRbf() || i.isReplaceByFeeEnabled() || silentPaymentTransaction)
|
||||
.map(txInput -> walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst().get())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
@@ -242,12 +244,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
.collect(Collectors.toList());
|
||||
|
||||
boolean consolidationTransaction = consolidationOutputs.size() == blockTransaction.getTransaction().getOutputs().size() && consolidationOutputs.size() == 1;
|
||||
boolean safeToAddInputsOrOutputs = transactionEntry.getWallet().isSafeToAddInputsOrOutputs(blockTransaction);
|
||||
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||
Transaction tx = blockTransaction.getTransaction();
|
||||
double vSize = tx.getVirtualSize();
|
||||
if(changeTotal == 0) {
|
||||
//Add change output length to vSize if change was not present on the original transaction
|
||||
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getOutputScript());
|
||||
TransactionOutput changeOutput = new TransactionOutput(new Transaction(), 1L, transactionEntry.getWallet().getNode(KeyPurpose.CHANGE).getOutputScript());
|
||||
vSize += changeOutput.getLength();
|
||||
}
|
||||
double inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? (double)tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
|
||||
@@ -256,7 +259,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
List<OutputGroup> outputGroups = transactionEntry.getWallet().getGroupedUtxos(txoFilters, feeRate, AppServices.getMinimumRelayFeeRate(), Config.get().isGroupByAddress())
|
||||
.stream().filter(outputGroup -> outputGroup.getEffectiveValue() >= 0).collect(Collectors.toList());
|
||||
Collections.shuffle(outputGroups);
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction) {
|
||||
while((double)changeTotal / vSize < getMaxFeeRate() && !outputGroups.isEmpty() && !cancelTransaction && !consolidationTransaction && safeToAddInputsOrOutputs) {
|
||||
//If there is insufficient change output, include another random output group so the fee can be increased
|
||||
OutputGroup outputGroup = outputGroups.remove(0);
|
||||
for(BlockTransactionHashIndex utxo : outputGroup.getUtxos()) {
|
||||
@@ -297,9 +300,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
label += " (Replaced By Fee)";
|
||||
}
|
||||
|
||||
if(txOutput.getScript().getToAddress() != null) {
|
||||
Address address = txOutput.getScript().getToAddress();
|
||||
if(address != null) {
|
||||
long value = txOutput.getValue();
|
||||
//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);
|
||||
boolean sendMax = blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0;
|
||||
SilentPaymentAddress silentPaymentAddress = transactionEntry.getWallet().getSilentPaymentAddress(address);
|
||||
return silentPaymentAddress == null ? new Payment(address, label, value, sendMax) : new SilentPayment(silentPaymentAddress, label, value, sendMax);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -328,15 +335,17 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
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);
|
||||
Payment payment = transactionEntry.getWallet().getPolicyType() == PolicyType.SINGLE_SP ?
|
||||
new SilentPayment(transactionEntry.getWallet().getSilentPaymentScanAddress().getChangeAddress().getSilentPaymentAddress(),
|
||||
existing.getLabel(), existing.getAmount(), true) :
|
||||
new Payment(transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress(), existing.getLabel(), existing.getAmount(), true);
|
||||
payments.clear();
|
||||
payments.add(payment);
|
||||
opReturns.clear();
|
||||
}
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, payments, opReturns.isEmpty() ? null : opReturns, rbfFee, true, blockTransaction, safeToAddInputsOrOutputs)));
|
||||
}
|
||||
|
||||
private static Double getMaxFeeRate() {
|
||||
@@ -363,10 +372,10 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
}
|
||||
|
||||
BlockTransactionHashIndex cpfpUtxo = ourOutputs.get(0);
|
||||
Address freshAddress = transactionEntry.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress();
|
||||
TransactionOutput txOutput = new TransactionOutput(new Transaction(), cpfpUtxo.getValue(), freshAddress.getOutputScript());
|
||||
long dustThreshold = freshAddress.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||
double inputSize = freshAddress.getScriptType().getInputVbytes();
|
||||
Address receiveAddress = transactionEntry.getWallet().getNode(KeyPurpose.RECEIVE).getAddress();
|
||||
TransactionOutput txOutput = new TransactionOutput(new Transaction(), cpfpUtxo.getValue(), receiveAddress.getOutputScript());
|
||||
long dustThreshold = receiveAddress.getScriptType().getDustThreshold(txOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||
double inputSize = receiveAddress.getScriptType().getInputVbytes();
|
||||
double vSize = inputSize + txOutput.getLength();
|
||||
|
||||
List<TxoFilter> txoFilters = List.of(new ExcludeTxoFilter(List.of(cpfpUtxo)), new SpentTxoFilter(), new FrozenTxoFilter(), new CoinbaseTxoFilter(transactionEntry.getWallet()));
|
||||
@@ -390,19 +399,23 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
|
||||
label += (label.isEmpty() ? "" : " ") + "(CPFP)";
|
||||
Payment payment = new Payment(freshAddress, label, inputTotal, true);
|
||||
Payment payment = transactionEntry.getWallet().getPolicyType() == PolicyType.SINGLE_SP ?
|
||||
new SilentPayment(transactionEntry.getWallet().getSilentPaymentScanAddress().getChangeAddress().getSilentPaymentAddress(),
|
||||
label, inputTotal, true) :
|
||||
new Payment(transactionEntry.getWallet().getFreshNode(KeyPurpose.CHANGE).getAddress(), label, inputTotal, true);
|
||||
|
||||
EventManager.get().post(new SendActionEvent(transactionEntry.getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null)));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(transactionEntry.getWallet(), utxos, List.of(payment), null, blockTransaction.getFee(), true, null, true)));
|
||||
}
|
||||
|
||||
private static boolean canRBF(BlockTransaction blockTransaction) {
|
||||
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee();
|
||||
private static boolean canRBF(BlockTransaction blockTransaction, Wallet wallet) {
|
||||
return Config.get().isMempoolFullRbf() || blockTransaction.getTransaction().isReplaceByFee() || wallet.isSilentPaymentsTransaction(blockTransaction);
|
||||
}
|
||||
|
||||
private static boolean canSignMessage(WalletNode walletNode) {
|
||||
Wallet wallet = walletNode.getWallet();
|
||||
return wallet.getKeystores().size() == 1 && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
PolicyType policyType = wallet.getPolicyType();
|
||||
return (policyType == PolicyType.SINGLE_HD || policyType == PolicyType.SINGLE_SP) && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||
@@ -459,7 +472,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
Double feeRate = transactionEntry.getBlockTransaction().getFeeRate();
|
||||
Long vSizefromTip = transactionEntry.getVSizeFromTip();
|
||||
if(feeRate != null && vSizefromTip != null) {
|
||||
long blocksFromTip = (long)Math.ceil((double)vSizefromTip / Transaction.MAX_BLOCK_SIZE);
|
||||
long blocksFromTip = (long)Math.ceil((double)vSizefromTip / Transaction.MAX_BLOCK_SIZE_VBYTES);
|
||||
|
||||
String amount = vSizefromTip + " vB";
|
||||
if(vSizefromTip > 1000 * 1000) {
|
||||
@@ -475,7 +488,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
tooltip += "\nFee rate: " + String.format("%.2f", feeRate) + " sats/vB";
|
||||
}
|
||||
|
||||
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction()) ? "Enabled" : "Disabled");
|
||||
tooltip += "\nRBF: " + (canRBF(transactionEntry.getBlockTransaction(), transactionEntry.getWallet()) ? "Enabled" : "Disabled");
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
@@ -543,6 +556,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
private static class UnconfirmedTransactionContextMenu extends ContextMenu {
|
||||
public UnconfirmedTransactionContextMenu(TransactionEntry transactionEntry) {
|
||||
Wallet wallet = transactionEntry.getWallet();
|
||||
BlockTransaction blockTransaction = transactionEntry.getBlockTransaction();
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
@@ -552,7 +566,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
});
|
||||
getItems().add(viewTransaction);
|
||||
|
||||
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem increaseFee = new MenuItem("Increase Fee (RBF)");
|
||||
increaseFee.setGraphic(getIncreaseFeeRBFGlyph());
|
||||
increaseFee.setOnAction(AE -> {
|
||||
@@ -563,7 +577,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
getItems().add(increaseFee);
|
||||
}
|
||||
|
||||
if(canRBF(blockTransaction) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
if(canRBF(blockTransaction, wallet) && Config.get().isIncludeMempoolOutputs() && transactionEntry.getWallet().allInputsFromWallet(blockTransaction.getHash())) {
|
||||
MenuItem cancelTx = new MenuItem("Cancel Transaction (RBF)");
|
||||
cancelTx.setGraphic(getCancelTransactionRBFGlyph());
|
||||
cancelTx.setOnAction(AE -> {
|
||||
@@ -656,7 +670,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
public static class AddressContextMenu extends ContextMenu {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry, boolean addUtxoItems, TreeTableView<Entry> treetable) {
|
||||
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
|
||||
if(nodeEntry == null || (!nodeEntry.getWallet().isBip47() && nodeEntry.getWallet().getPolicyType() != PolicyType.SINGLE_SP)) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
receiveToAddress.setOnAction(event -> {
|
||||
@@ -805,18 +819,17 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
cell.getStyleClass().remove("utxo-row");
|
||||
cell.getStyleClass().remove("unconfirmed-row");
|
||||
cell.getStyleClass().remove("summary-row");
|
||||
cell.getStyleClass().remove("address-cell");
|
||||
boolean addressCell = cell.getStyleClass().remove("address-cell");
|
||||
cell.getStyleClass().remove("hashindex-row");
|
||||
cell.getStyleClass().remove("confirming");
|
||||
cell.getStyleClass().remove("negative-amount");
|
||||
cell.getStyleClass().remove("spent");
|
||||
cell.getStyleClass().remove("unspendable");
|
||||
cell.getStyleClass().remove("number-field");
|
||||
|
||||
if(entry != null) {
|
||||
if(entry instanceof TransactionEntry) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
cell.getStyleClass().add("transaction-row");
|
||||
TransactionEntry transactionEntry = (TransactionEntry)entry;
|
||||
|
||||
if(cell instanceof ConfirmationsListener confirmationsListener) {
|
||||
if(transactionEntry.isConfirming()) {
|
||||
cell.getStyleClass().add("confirming");
|
||||
@@ -825,25 +838,36 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
confirmationsListener.getConfirmationsProperty().unbind();
|
||||
}
|
||||
}
|
||||
if(OsType.getCurrent() == OsType.MACOS && transactionEntry.getBlockTransaction().getHeight() > 0 && !cell.getStyleClass().contains("label-cell")) {
|
||||
cell.getStyleClass().add("number-field");
|
||||
}
|
||||
} else if(entry instanceof NodeEntry) {
|
||||
cell.getStyleClass().add("node-row");
|
||||
} else if(entry instanceof UtxoEntry) {
|
||||
} else if(entry instanceof UtxoEntry utxoEntry) {
|
||||
cell.getStyleClass().add("utxo-row");
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
if(!utxoEntry.isSpendable()) {
|
||||
cell.getStyleClass().add("unspendable");
|
||||
}
|
||||
} else if(entry instanceof HashIndexEntry) {
|
||||
if(OsType.getCurrent() == OsType.MACOS && utxoEntry.getHashIndex().getHeight() > 0 && !addressCell && !cell.getStyleClass().contains("label-cell")) {
|
||||
cell.getStyleClass().add("number-field");
|
||||
}
|
||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
cell.getStyleClass().add("hashindex-row");
|
||||
HashIndexEntry hashIndexEntry = (HashIndexEntry)entry;
|
||||
if(hashIndexEntry.isSpent()) {
|
||||
cell.getStyleClass().add("spent");
|
||||
}
|
||||
} else if(entry instanceof WalletSummaryDialog.UnconfirmedEntry) {
|
||||
cell.getStyleClass().add("unconfirmed-row");
|
||||
} else if(entry instanceof WalletSummaryDialog.SummaryEntry) {
|
||||
} else if(entry instanceof WalletSummaryDialog.SummaryEntry || entry instanceof WalletSummaryDialog.AllSummaryEntry) {
|
||||
cell.getStyleClass().add("summary-row");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTableSizeRecalculation() {
|
||||
//As per https://bugs.openjdk.org/browse/JDK-8265669 we check for cell visibility to avoid unnecessary recalculation, but this can result in false positives
|
||||
//The method releaseCell in VirtualFlow is responsible for setting accumCell visibility to false after use, so check this method is calling updateItem
|
||||
return StackWalker.getInstance().walk(frames -> frames.anyMatch(frame -> frame.getClassName().equals("javafx.scene.control.skin.VirtualFlow")
|
||||
&& frame.getMethodName().equals("releaseCell")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.FeeRatesSource;
|
||||
import javafx.application.Platform;
|
||||
@@ -7,6 +8,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.Slider;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -14,9 +16,11 @@ import static com.sparrowwallet.sparrow.AppServices.*;
|
||||
|
||||
public class FeeRangeSlider extends Slider {
|
||||
private static final double FEE_RATE_SCROLL_INCREMENT = 0.01;
|
||||
private static final DecimalFormat INTEGER_FEE_RATE_FORMAT = new DecimalFormat("0");
|
||||
private static final DecimalFormat FRACTIONAL_FEE_RATE_FORMAT = new DecimalFormat("0.###");
|
||||
|
||||
public FeeRangeSlider() {
|
||||
super(0, FEE_RATES_RANGE.size() - 1, 0);
|
||||
super(0, AppServices.getFeeRatesRange().size() - 1, 0);
|
||||
setMajorTickUnit(1);
|
||||
setMinorTickCount(0);
|
||||
setSnapToTicks(false);
|
||||
@@ -27,11 +31,11 @@ public class FeeRangeSlider extends Slider {
|
||||
setLabelFormatter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Double object) {
|
||||
Long feeRate = LONG_FEE_RATES_RANGE.get(object.intValue());
|
||||
Double feeRate = AppServices.getLongFeeRatesRange().get(object.intValue());
|
||||
if(isLongFeeRange() && feeRate >= 1000) {
|
||||
return feeRate / 1000 + "k";
|
||||
return INTEGER_FEE_RATE_FORMAT.format(feeRate / 1000) + "k";
|
||||
}
|
||||
return Long.toString(feeRate);
|
||||
return feeRate > 0d && feeRate < Transaction.DEFAULT_MIN_RELAY_FEE ? FRACTIONAL_FEE_RATE_FORMAT.format(feeRate) : INTEGER_FEE_RATE_FORMAT.format(feeRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,10 +55,10 @@ public class FeeRangeSlider extends Slider {
|
||||
setOnScroll(event -> {
|
||||
if(event.getDeltaY() != 0) {
|
||||
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
|
||||
if(newFeeRate < LONG_FEE_RATES_RANGE.get(0)) {
|
||||
newFeeRate = LONG_FEE_RATES_RANGE.get(0);
|
||||
} else if(newFeeRate > LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1)) {
|
||||
newFeeRate = LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1);
|
||||
if(newFeeRate < AppServices.getLongFeeRatesRange().getFirst()) {
|
||||
newFeeRate = AppServices.getLongFeeRatesRange().getFirst();
|
||||
} else if(newFeeRate > AppServices.getLongFeeRatesRange().getLast()) {
|
||||
newFeeRate = AppServices.getLongFeeRatesRange().getLast();
|
||||
}
|
||||
setFeeRate(newFeeRate);
|
||||
}
|
||||
@@ -62,27 +66,79 @@ public class FeeRangeSlider extends Slider {
|
||||
}
|
||||
|
||||
public double getFeeRate() {
|
||||
return Math.pow(2.0, getValue());
|
||||
return getFeeRate(AppServices.getMinimumRelayFeeRate());
|
||||
}
|
||||
|
||||
public double getFeeRate(Double minRelayFeeRate) {
|
||||
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
return Math.pow(2.0, getValue());
|
||||
}
|
||||
|
||||
if(getValue() < 1.0d) {
|
||||
if(minRelayFeeRate == 0.0d) {
|
||||
return getValue();
|
||||
}
|
||||
return Math.pow(minRelayFeeRate, 1.0d - getValue());
|
||||
}
|
||||
|
||||
return Math.pow(2.0, getValue() - 1.0d);
|
||||
}
|
||||
|
||||
public void setFeeRate(double feeRate) {
|
||||
double value = Math.log(feeRate) / Math.log(2);
|
||||
setFeeRate(feeRate, AppServices.getMinimumRelayFeeRate());
|
||||
}
|
||||
|
||||
public void setFeeRate(double feeRate, Double minRelayFeeRate) {
|
||||
double value = getValue(feeRate, minRelayFeeRate);
|
||||
updateMaxFeeRange(value);
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
private double getValue(double feeRate, Double minRelayFeeRate) {
|
||||
double value;
|
||||
|
||||
if(minRelayFeeRate >= Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
value = Math.log(feeRate) / Math.log(2);
|
||||
} else {
|
||||
if(feeRate < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
if(minRelayFeeRate == 0.0d) {
|
||||
return feeRate;
|
||||
}
|
||||
value = 1.0d - (Math.log(feeRate) / Math.log(minRelayFeeRate));
|
||||
} else {
|
||||
value = (Math.log(feeRate) / Math.log(2.0)) + 1.0d;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public void updateFeeRange(Double minRelayFeeRate, Double previousMinRelayFeeRate) {
|
||||
if(minRelayFeeRate != null && previousMinRelayFeeRate != null) {
|
||||
setFeeRate(getFeeRate(previousMinRelayFeeRate), minRelayFeeRate);
|
||||
}
|
||||
setMinorTickCount(1);
|
||||
setMinorTickCount(0);
|
||||
}
|
||||
|
||||
private void updateMaxFeeRange(double value) {
|
||||
if(value >= getMax() && !isLongFeeRange()) {
|
||||
setMax(LONG_FEE_RATES_RANGE.size() - 1);
|
||||
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
setMin(1.0d);
|
||||
}
|
||||
setMax(AppServices.getLongFeeRatesRange().size() - 1);
|
||||
updateTrackHighlight();
|
||||
} else if(value == getMin() && isLongFeeRange()) {
|
||||
setMax(FEE_RATES_RANGE.size() - 1);
|
||||
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
setMin(0.0d);
|
||||
}
|
||||
setMax(AppServices.getFeeRatesRange().size() - 1);
|
||||
updateTrackHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLongFeeRange() {
|
||||
return getMax() > FEE_RATES_RANGE.size() - 1;
|
||||
public boolean isLongFeeRange() {
|
||||
return getMax() > AppServices.getFeeRatesRange().size() - 1;
|
||||
}
|
||||
|
||||
public void updateTrackHighlight() {
|
||||
@@ -137,9 +193,9 @@ public class FeeRangeSlider extends Slider {
|
||||
}
|
||||
|
||||
private int getPercentageOfFeeRange(Double feeRate) {
|
||||
double index = Math.log(feeRate) / Math.log(2);
|
||||
double index = getValue(feeRate, AppServices.getMinimumRelayFeeRate());
|
||||
if(isLongFeeRange()) {
|
||||
index *= ((double)FEE_RATES_RANGE.size() / (LONG_FEE_RATES_RANGE.size())) * 0.99;
|
||||
index *= ((double)AppServices.getFeeRatesRange().size() / (AppServices.getLongFeeRatesRange().size())) * 0.99;
|
||||
}
|
||||
return (int)Math.round(index * 10.0);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.CurrencyRate;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
@@ -10,7 +12,6 @@ import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Currency;
|
||||
@@ -24,7 +25,7 @@ public class FiatCell extends TreeTableCell<Entry, Number> {
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new FiatContextMenu();
|
||||
getStyleClass().add("coin-cell");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
getStyleClass().add("number-field");
|
||||
}
|
||||
}
|
||||
@@ -47,20 +48,27 @@ public class FiatCell extends TreeTableCell<Entry, Number> {
|
||||
CurrencyRate currencyRate = coinTreeTable.getCurrencyRate();
|
||||
|
||||
if(currencyRate != null && currencyRate.isAvailable()) {
|
||||
Currency currency = currencyRate.getCurrency();
|
||||
double btcRate = currencyRate.getBtcRate();
|
||||
if(Config.get().isHideAmounts()) {
|
||||
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
setContextMenu(null);
|
||||
} else {
|
||||
Currency currency = currencyRate.getCurrency();
|
||||
double btcRate = currencyRate.getBtcRate();
|
||||
|
||||
BigDecimal satsBalance = BigDecimal.valueOf(amount.longValue());
|
||||
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
|
||||
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(btcRate));
|
||||
BigDecimal satsBalance = BigDecimal.valueOf(amount.longValue());
|
||||
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
|
||||
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(btcRate));
|
||||
|
||||
String label = format.formatCurrencyValue(fiatBalance.doubleValue());
|
||||
tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate));
|
||||
String label = format.formatCurrencyValue(fiatBalance.doubleValue());
|
||||
tooltip.setText("1 BTC = " + currency.getSymbol() + " " + format.formatCurrencyValue(btcRate));
|
||||
|
||||
setText(label);
|
||||
setGraphic(null);
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
setText(label);
|
||||
setGraphic(null);
|
||||
setTooltip(tooltip);
|
||||
setContextMenu(contextMenu);
|
||||
}
|
||||
} else {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
|
||||
@@ -90,6 +90,13 @@ public class FiatLabel extends CopyableLabel {
|
||||
|
||||
private void setValueAsText(long balance, UnitFormat unitFormat) {
|
||||
if(getCurrency() != null && getBtcRate() > 0.0) {
|
||||
if(Config.get().isHideAmounts()) {
|
||||
setText(CoinLabel.HIDDEN_AMOUNT_TEXT);
|
||||
setTooltip(null);
|
||||
setContextMenu(null);
|
||||
return;
|
||||
}
|
||||
|
||||
BigDecimal satsBalance = BigDecimal.valueOf(balance);
|
||||
BigDecimal btcBalance = satsBalance.divide(BigDecimal.valueOf(Transaction.SATOSHIS_PER_BITCOIN));
|
||||
BigDecimal fiatBalance = btcBalance.multiply(BigDecimal.valueOf(getBtcRate()));
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.FileImport;
|
||||
@@ -24,9 +26,7 @@ import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.control.textfield.CustomPasswordField;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -45,8 +45,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
private final boolean fileFormatAvailable;
|
||||
protected List<Wallet> wallets;
|
||||
|
||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
|
||||
super(title, description, content, imageUrl);
|
||||
public FileImportPane(FileImport importer, String title, String description, String content, WalletModel walletModel, boolean scannable, boolean fileFormatAvailable) {
|
||||
super(title, description, content, walletModel);
|
||||
this.importer = importer;
|
||||
this.scannable = scannable;
|
||||
this.fileFormatAvailable = fileFormatAvailable;
|
||||
@@ -104,7 +104,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open " + importer.getWalletModel().toDisplayString() + " File");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", Platform.getCurrent().equals(Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("JSON", "*.json"),
|
||||
new FileChooser.ExtensionFilter("TXT", "*.txt")
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
|
||||
private final boolean file;
|
||||
|
||||
public FileKeystoreExportPane(Keystore keystore, KeystoreFileExport exporter) {
|
||||
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||
super(exporter.getName(), "Keystore export", exporter.getKeystoreExportDescription(), exporter.getWalletModel());
|
||||
this.keystore = keystore;
|
||||
this.exporter = exporter;
|
||||
this.scannable = exporter.isKeystoreExportScannable();
|
||||
@@ -157,7 +157,7 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
|
||||
if(exporter instanceof Bip129) {
|
||||
UR ur = UR.fromBytes(baos.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, baos.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, QREncoding.UR);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(baos.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
private final KeyDerivation requiredDerivation;
|
||||
|
||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
super(importer, importer.getName(), "Key import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
@@ -25,7 +25,7 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||
Keystore keystore = getScannedKeystore(wallet.getScriptType());
|
||||
if(keystore == null) {
|
||||
keystore = importer.getKeystore(wallet.getScriptType(), inputStream, password);
|
||||
keystore = importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType(), inputStream, password);
|
||||
}
|
||||
|
||||
if(requiredDerivation != null && !requiredDerivation.getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryItem;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -32,7 +32,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.sparrowwallet.sparrow.wallet.SettingsController.getCryptoOutput;
|
||||
import static com.sparrowwallet.sparrow.wallet.SettingsController.getUROutputDescriptor;
|
||||
|
||||
public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
private final Wallet wallet;
|
||||
@@ -41,7 +41,7 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
private final boolean file;
|
||||
|
||||
public FileWalletExportPane(Wallet wallet, WalletExport exporter) {
|
||||
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), "image/" + exporter.getWalletModel().getType() + ".png");
|
||||
super(exporter.getName(), "Wallet export", exporter.getWalletExportDescription(), exporter.getWalletModel());
|
||||
this.wallet = wallet;
|
||||
this.exporter = exporter;
|
||||
this.scannable = exporter.isWalletExportScannable();
|
||||
@@ -168,21 +168,21 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), false);
|
||||
} else if(exporter instanceof Bip129) {
|
||||
} else if(exporter instanceof Bip129 || exporter instanceof WalletLabels) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, QREncoding.UR);
|
||||
} else if(exporter instanceof Descriptor) {
|
||||
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
|
||||
boolean selectBbqrOption = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr());
|
||||
QREncoding encoding = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()) ? QREncoding.BBQR : QREncoding.UR;
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
|
||||
RegistryItem registryItem = getUROutputDescriptor(exportWallet);
|
||||
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, selectBbqrOption);
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), registryItem.toUR(), bbqr, encoding);
|
||||
} else if(exporter.getClass().equals(ColdcardMultisig.class)) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, true);
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, QREncoding.BBQR);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@@ -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(), importer.isWalletImportFileFormatAvailable());
|
||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), importer.getWalletModel(), importer.isWalletImportScannable(), importer.isWalletImportFileFormatAvailable());
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(FileWalletKeystoreImportPane.class);
|
||||
@@ -42,7 +42,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private String password;
|
||||
|
||||
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
@@ -50,19 +50,27 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
this.fileName = fileName;
|
||||
this.password = password;
|
||||
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||
List<PolicyAndScriptType> types = new ArrayList<>();
|
||||
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
|
||||
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
|
||||
types.add(new PolicyAndScriptType(policyType, scriptType));
|
||||
}
|
||||
}
|
||||
|
||||
if(wallets != null && !wallets.isEmpty()) {
|
||||
if(wallets.size() == 1 && scriptTypes.contains(wallets.get(0).getScriptType())) {
|
||||
Wallet wallet = wallets.get(0);
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
|
||||
wallets.stream().filter(w -> w.getPolicyType() == null).forEach(w -> w.setPolicyType(PolicyType.SINGLE_HD));
|
||||
List<PolicyAndScriptType> walletTypes = wallets.stream().map(w -> new PolicyAndScriptType(w.getPolicyType(), w.getScriptType())).toList();
|
||||
types.retainAll(walletTypes);
|
||||
if(types.isEmpty()) {
|
||||
throw new ImportException("No singlesig script types present in QR code");
|
||||
}
|
||||
|
||||
if(types.size() == 1) {
|
||||
Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == types.getFirst().policyType() && w.getScriptType() == types.getFirst().scriptType()).findFirst().orElseThrow(ImportException::new);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), null));
|
||||
wallet.setName(importer.getName());
|
||||
EventManager.get().post(new WalletImportEvent(wallets.get(0)));
|
||||
} else {
|
||||
scriptTypes.retainAll(wallets.stream().map(Wallet::getScriptType).collect(Collectors.toList()));
|
||||
if(scriptTypes.isEmpty()) {
|
||||
throw new ImportException("No singlesig script types present in QR code");
|
||||
}
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
@@ -72,58 +80,61 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
}
|
||||
}
|
||||
|
||||
setContent(getScriptTypeEntry(scriptTypes));
|
||||
setContent(getScriptTypeEntry(types));
|
||||
setExpanded(true);
|
||||
importButton.setDisable(true);
|
||||
}
|
||||
|
||||
private void importWallet(ScriptType scriptType) throws ImportException {
|
||||
private void importWallet(PolicyAndScriptType type) throws ImportException {
|
||||
PolicyType policyType = type.policyType();
|
||||
ScriptType scriptType = type.scriptType();
|
||||
|
||||
if(wallets != null && !wallets.isEmpty()) {
|
||||
Wallet wallet = wallets.stream().filter(wallet1 -> wallet1.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
|
||||
Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == policyType && w.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
|
||||
wallet.setName(importer.getName());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
||||
Keystore keystore = importer.getKeystore(scriptType, bais, password);
|
||||
Keystore keystore = importer.getKeystore(policyType, scriptType, bais, password);
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(Files.getNameWithoutExtension(fileName));
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setPolicyType(policyType);
|
||||
wallet.setScriptType(scriptType);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), null));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), null));
|
||||
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
}
|
||||
}
|
||||
|
||||
private Node getScriptTypeEntry(List<ScriptType> scriptTypes) {
|
||||
Label label = new Label("Script Type:");
|
||||
private Node getScriptTypeEntry(List<PolicyAndScriptType> types) {
|
||||
Label label = new Label("Type:");
|
||||
|
||||
HBox fieldBox = new HBox(5);
|
||||
fieldBox.setAlignment(Pos.CENTER_RIGHT);
|
||||
ComboBox<ScriptType> scriptTypeComboBox = new ComboBox<>(FXCollections.observableArrayList(scriptTypes));
|
||||
if(scriptTypes.contains(ScriptType.P2WPKH)) {
|
||||
scriptTypeComboBox.setValue(ScriptType.P2WPKH);
|
||||
ComboBox<PolicyAndScriptType> comboBox = new ComboBox<>(FXCollections.observableArrayList(types));
|
||||
PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH);
|
||||
if(types.contains(defaultType)) {
|
||||
comboBox.setValue(defaultType);
|
||||
}
|
||||
scriptTypeComboBox.setConverter(new StringConverter<>() {
|
||||
comboBox.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(ScriptType scriptType) {
|
||||
return scriptType == null ? "" : scriptType.getDescription();
|
||||
public String toString(PolicyAndScriptType type) {
|
||||
return type == null ? "" : type.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType fromString(String string) {
|
||||
public PolicyAndScriptType fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
scriptTypeComboBox.setMaxWidth(170);
|
||||
comboBox.setMaxWidth(220);
|
||||
|
||||
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);
|
||||
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is newer and supports both HD and SP (silent payments) wallets.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
fieldBox.getChildren().addAll(comboBox, helpLabel);
|
||||
|
||||
Region region = new Region();
|
||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||
@@ -133,7 +144,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
try {
|
||||
importWallet(scriptTypeComboBox.getValue());
|
||||
importWallet(comboBox.getValue());
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing file", e);
|
||||
String errorMessage = e.getMessage();
|
||||
@@ -154,8 +165,14 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
Platform.runLater(scriptTypeComboBox::requestFocus);
|
||||
Platform.runLater(comboBox::requestFocus);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
|
||||
public String getDescription() {
|
||||
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHash;
|
||||
import com.sparrowwallet.drongo.wallet.Persistable;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.event.Event;
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.cell.TextFieldTreeTableCell;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.DataFormat;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.converter.DefaultStringConverter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -34,12 +38,23 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||
if(empty) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
} else {
|
||||
Entry entry = getTreeTableView().getTreeItem(getIndex()).getValue();
|
||||
EntryCell.applyRowStyles(this, entry);
|
||||
|
||||
setText(label);
|
||||
setContextMenu(new LabelContextMenu(entry, label));
|
||||
|
||||
double width = label == null || label.length() < 20 ? 0.0 : TextUtils.computeTextWidth(getFont(), label, 0.0D);
|
||||
if(width > getTableColumn().getWidth()) {
|
||||
Tooltip tooltip = new Tooltip(label);
|
||||
tooltip.setMaxWidth(getTreeTableView().getWidth());
|
||||
tooltip.setWrapText(true);
|
||||
setTooltip(tooltip);
|
||||
} else {
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +62,20 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||
public void commitEdit(String label) {
|
||||
if(label != null) {
|
||||
label = label.trim();
|
||||
if(label.length() > Persistable.MAX_LABEL_LENGTH) {
|
||||
label = label.substring(0, Persistable.MAX_LABEL_LENGTH);
|
||||
Platform.runLater(() -> {
|
||||
Point2D p = this.localToScene(0.0, 0.0);
|
||||
final Tooltip truncateTooltip = new Tooltip();
|
||||
truncateTooltip.setText("Labels are truncated at " + Persistable.MAX_LABEL_LENGTH + " characters");
|
||||
truncateTooltip.setAutoHide(true);
|
||||
truncateTooltip.show(this, p.getX() + this.getScene().getX() + this.getScene().getWindow().getX() + this.getHeight(),
|
||||
p.getY() + this.getScene().getY() + this.getScene().getWindow().getY() + this.getHeight());
|
||||
PauseTransition pt = new PauseTransition(Duration.millis(2000));
|
||||
pt.setOnFinished(_ -> truncateTooltip.hide());
|
||||
pt.play();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// This block is necessary to support commit on losing focus, because
|
||||
@@ -103,7 +132,7 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||
return confirmationsProperty;
|
||||
}
|
||||
|
||||
private static class LabelContextMenu extends ContextMenu {
|
||||
private class LabelContextMenu extends ContextMenu {
|
||||
public LabelContextMenu(Entry entry, String label) {
|
||||
MenuItem copyLabel = new MenuItem("Copy Label");
|
||||
copyLabel.setOnAction(AE -> {
|
||||
@@ -123,6 +152,13 @@ class LabelCell extends TextFieldTreeTableCell<Entry, String> implements Confirm
|
||||
}
|
||||
});
|
||||
getItems().add(pasteLabel);
|
||||
|
||||
MenuItem editLabel = new MenuItem("Edit Label...");
|
||||
editLabel.setOnAction(AE -> {
|
||||
hide();
|
||||
startEdit();
|
||||
});
|
||||
getItems().add(editLabel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
@@ -57,7 +58,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
stage.setResizable(false);
|
||||
|
||||
StackPane scenePane = new StackPane();
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
if(OsType.getCurrent() == OsType.WINDOWS) {
|
||||
scenePane.setBorder(new Border(new BorderStroke(Color.DARKGRAY, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT)));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
@@ -9,7 +10,12 @@ import com.sparrowwallet.drongo.crypto.Bip322;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
@@ -17,10 +23,11 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
@@ -32,17 +39,21 @@ import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
|
||||
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
|
||||
|
||||
private static final Pattern signedMessagePattern = Pattern.compile("-----BEGIN BITCOIN SIGNED MESSAGE-----\\r?\\n(.*)\\r?\\n-----BEGIN BITCOIN SIGNATURE-----\\r?\\n(.*)\\r?\\n(.*)\\r?\\n-----END BITCOIN SIGNATURE-----\r?\n?");
|
||||
|
||||
private final TextField address;
|
||||
private final TextArea message;
|
||||
private final TextArea signature;
|
||||
@@ -104,19 +115,13 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
this.wallet = wallet;
|
||||
this.walletNode = walletNode;
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
final DialogPane dialogPane = new MessageSignDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText(title == null ? (wallet == null ? "Verify Message" : "Sign/Verify Message") : title);
|
||||
|
||||
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);
|
||||
}
|
||||
dialogPane.setGraphic(new WalletModelImage(WalletModel.SEED));
|
||||
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(20);
|
||||
@@ -132,6 +137,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
address.getStyleClass().add("id");
|
||||
address.setEditable(walletNode == null);
|
||||
address.setTooltip(new Tooltip("Only singlesig addresses can sign"));
|
||||
address.setSkin(new AddressTextFieldSkin(address));
|
||||
addressField.getInputs().add(address);
|
||||
|
||||
if(walletNode != null) {
|
||||
@@ -199,13 +205,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
} else {
|
||||
dialogPane.getButtonTypes().addAll(showQrButtonType, signButtonType, verifyButtonType, doneButtonType);
|
||||
|
||||
Button showQrButton = (Button) dialogPane.lookupButton(showQrButtonType);
|
||||
showQrButton.setDisable(wallet == null);
|
||||
showQrButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
|
||||
showQrButton.setGraphicTextGap(5);
|
||||
showQrButton.setOnAction(event -> {
|
||||
showQr();
|
||||
});
|
||||
Node showQrButton = dialogPane.lookupButton(showQrButtonType);
|
||||
|
||||
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
|
||||
signButton.setDisable(!canSign);
|
||||
@@ -244,12 +244,22 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
setFormatFromScriptType(address.getScriptType());
|
||||
if(wallet != null) {
|
||||
setWalletNodeFromAddress(wallet, address);
|
||||
if(walletNode != null) {
|
||||
setFormatFromScriptType(walletNode.getWallet().getScriptType());
|
||||
}
|
||||
}
|
||||
} catch(InvalidAddressException e) {
|
||||
//can't happen
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
formatGroup.selectedToggleProperty().addListener((_, _, newVal) -> {
|
||||
if(wallet != null) {
|
||||
boolean canSignSelectedFormat = canSignAllFormats(wallet) || newVal == formatElectrum;
|
||||
signButton.setDisable(!isValidAddress() || !canSign || !canSignSelectedFormat);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
EventManager.get().register(this);
|
||||
@@ -267,7 +277,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
setResultConverter(dialogButton -> dialogButton == showQrButtonType || dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if(address.getText().isEmpty()) {
|
||||
@@ -277,7 +287,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
if(wallet != null && walletNode != null) {
|
||||
setFormatFromScriptType(wallet.getScriptType());
|
||||
setFormatFromScriptType(walletNode.getWallet().getScriptType());
|
||||
} else {
|
||||
formatGroup.selectToggle(formatElectrum);
|
||||
}
|
||||
@@ -285,15 +295,19 @@ 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().size() != 1 || (wallet.getPolicyType() != PolicyType.SINGLE_HD && wallet.getPolicyType() != PolicyType.SINGLE_SP)) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using this wallet type");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canSign(Wallet wallet) {
|
||||
return wallet.getKeystores().get(0).hasPrivateKey()
|
||||
|| wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB
|
||||
|| wallet.getKeystores().get(0).getWalletModel().isCard();
|
||||
return wallet.getKeystores().getFirst().hasPrivateKey()
|
||||
|| wallet.getKeystores().getFirst().getSource() == KeystoreSource.HW_USB
|
||||
|| wallet.getKeystores().getFirst().getWalletModel().isCard();
|
||||
}
|
||||
|
||||
private boolean canSignAllFormats(Wallet wallet) {
|
||||
return wallet.getKeystores().getFirst().hasPrivateKey();
|
||||
}
|
||||
|
||||
private Address getAddress()throws InvalidAddressException {
|
||||
@@ -307,7 +321,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private boolean isValidAddress() {
|
||||
try {
|
||||
Address address = getAddress();
|
||||
return address.getScriptType().isAllowed(PolicyType.SINGLE) || address.getScriptType() == ScriptType.P2SH;
|
||||
return address.getScriptType().isAllowed(PolicyType.SINGLE_HD) || address.getScriptType() == ScriptType.P2SH;
|
||||
} catch (InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
@@ -349,7 +363,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
//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.getKeystores().getFirst().hasPrivateKey()) {
|
||||
if(signingWallet.isEncrypted()) {
|
||||
EventManager.get().post(new RequestOpenWalletsEvent());
|
||||
} else {
|
||||
@@ -362,19 +376,25 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||
try {
|
||||
Keystore keystore = decryptedWallet.getKeystores().get(0);
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
Keystore keystore = decryptedWallet.getKeystores().getFirst();
|
||||
String signatureText;
|
||||
if(isBip322()) {
|
||||
ScriptType scriptType = decryptedWallet.getScriptType();
|
||||
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
|
||||
if(decryptedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
ECKey spendPrivKey = keystore.getSpendPrivateKey(Collections.emptyMap());
|
||||
signatureText = Bip322.signMessageBip322Sp(walletNode.getAddress(), message.getText().trim(), spendPrivKey, walletNode.getSilentPaymentTweak());
|
||||
spendPrivKey.clear();
|
||||
} else {
|
||||
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
if(isBip322()) {
|
||||
ScriptType scriptType = decryptedWallet.getScriptType();
|
||||
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
|
||||
} else {
|
||||
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
|
||||
}
|
||||
privKey.clear();
|
||||
}
|
||||
signature.clear();
|
||||
signature.appendText(signatureText);
|
||||
privKey.clear();
|
||||
} catch(Exception e) {
|
||||
log.error("Could not sign message", e);
|
||||
AppServices.showErrorDialog("Could not sign message", e.getMessage());
|
||||
@@ -382,8 +402,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
private void signDeviceKeystore(Wallet deviceWallet) {
|
||||
List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
|
||||
KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
|
||||
List<String> fingerprints = List.of(deviceWallet.getKeystores().getFirst().getKeyDerivation().getMasterFingerprint());
|
||||
KeyDerivation fullDerivation = deviceWallet.getKeystores().getFirst().getKeyDerivation().extend(walletNode.getDerivation());
|
||||
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
|
||||
deviceSignMessageDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
|
||||
@@ -450,11 +470,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
if(scriptType == ScriptType.P2SH) {
|
||||
scriptType = ScriptType.P2SH_P2WPKH;
|
||||
}
|
||||
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).contains(scriptType)) {
|
||||
if(!ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).contains(scriptType)) {
|
||||
throw new IllegalArgumentException("Only single signature P2PKH, P2SH-P2WPKH or P2WPKH addresses can verify messages.");
|
||||
}
|
||||
|
||||
Address signedMessageAddress = scriptType.getAddress(signedMessageKey);
|
||||
Address signedMessageAddress = scriptType.getAddress(PolicyType.SINGLE_HD, signedMessageKey);
|
||||
return providedAddress.equals(signedMessageAddress);
|
||||
}
|
||||
|
||||
@@ -464,6 +484,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
return;
|
||||
}
|
||||
|
||||
if(isBip322()) {
|
||||
showBip322Qr();
|
||||
return;
|
||||
}
|
||||
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
|
||||
String derivationPath = KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), false);
|
||||
@@ -477,13 +502,88 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private void showBip322Qr() {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
PSBT psbt = buildBip322Psbt(signingWallet);
|
||||
|
||||
byte[] psbtBytes = psbt.getForExport().serialize();
|
||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||
BBQR bbqr = new BBQR(BBQRType.PSBT, psbtBytes);
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoPSBT.toUR(), bbqr, false, true, QREncoding.UR);
|
||||
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
|
||||
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
|
||||
scanQr();
|
||||
}
|
||||
}
|
||||
|
||||
private PSBT buildBip322Psbt(Wallet signingWallet) {
|
||||
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
Keystore keystore = signingWallet.getKeystores().getFirst();
|
||||
ECKey spendPubKey = keystore.getSilentPaymentScanAddress().getSpendKey();
|
||||
KeyDerivation spendDerivation = new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), KeyDerivation.writePath(KeyDerivation.getBip352SpendDerivation(keystore.getKeyDerivation().getDerivation())));
|
||||
return Bip322.getBip322PsbtSp(walletNode.getAddress(), message.getText().trim(), walletNode.getSilentPaymentTweak(), Map.of(spendPubKey, spendDerivation));
|
||||
}
|
||||
|
||||
PSBT psbt = Bip322.getBip322Psbt(signingWallet.getScriptType(), walletNode.getAddress(), message.getText().trim());
|
||||
addBip322DerivationInfo(psbt, signingWallet);
|
||||
|
||||
return psbt;
|
||||
}
|
||||
|
||||
private String extractBip322Signature(PSBT signedPsbt) {
|
||||
String psbtMessage = signedPsbt.getGenericSignedMessage();
|
||||
if(psbtMessage != null && !psbtMessage.equals(message.getText().trim())) {
|
||||
Optional<ButtonType> response = AppServices.showWarningDialog("Message mismatch",
|
||||
"The message in the signed PSBT does not match the message in this dialog.\n\nPSBT message: " + psbtMessage +
|
||||
"\n\nContinue extracting the signature?", ButtonType.NO, ButtonType.YES);
|
||||
if(response.isEmpty() || response.get() != ButtonType.YES) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
return Bip322.getBip322SignatureFromPsbtSp(signedPsbt);
|
||||
}
|
||||
|
||||
ECKey pubKey = signingWallet.getKeystores().getFirst().getPubKey(walletNode);
|
||||
return Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), signedPsbt, pubKey);
|
||||
}
|
||||
|
||||
private void addBip322DerivationInfo(PSBT psbt, Wallet signingWallet) {
|
||||
ScriptType scriptType = signingWallet.getScriptType();
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
|
||||
Keystore keystore = signingWallet.getKeystores().get(0);
|
||||
ECKey pubKey = keystore.getPubKey(walletNode);
|
||||
KeyDerivation fullDerivation = keystore.getKeyDerivation().extend(walletNode.getDerivation());
|
||||
|
||||
if(scriptType == ScriptType.P2TR) {
|
||||
psbtInput.setTapInternalKey(pubKey);
|
||||
psbtInput.getTapDerivedPublicKeys().put(ECKey.fromPublicOnly(pubKey.getPubKeyXCoord()), Map.of(fullDerivation, Collections.emptyList()));
|
||||
} else {
|
||||
psbtInput.getDerivedPublicKeys().put(scriptType.getOutputKey(signingWallet.getPolicyType(), pubKey), fullDerivation);
|
||||
}
|
||||
}
|
||||
|
||||
private void scanQr() {
|
||||
QRScanDialog qrScanDialog = new QRScanDialog();
|
||||
qrScanDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
|
||||
if(optionalResult.isPresent()) {
|
||||
QRScanDialog.Result result = optionalResult.get();
|
||||
if(result.payload != null) {
|
||||
if(result.psbt != null) {
|
||||
try {
|
||||
String sig = extractBip322Signature(result.psbt);
|
||||
if(sig != null) {
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error extracting BIP-322 signature from PSBT", e);
|
||||
AppServices.showErrorDialog("Error extracting signature", e.getMessage());
|
||||
}
|
||||
} else if(result.payload != null) {
|
||||
signature.clear();
|
||||
signature.appendText(result.payload);
|
||||
} else if(result.exception != null) {
|
||||
@@ -495,6 +595,132 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private void exportFile() {
|
||||
if(walletNode == null) {
|
||||
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
|
||||
return;
|
||||
}
|
||||
|
||||
if(isBip322()) {
|
||||
exportBip322File();
|
||||
return;
|
||||
}
|
||||
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
joiner.add(message.getText().trim().replaceAll("\r*\n*", ""));
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
|
||||
joiner.add(KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), true));
|
||||
joiner.add(walletNode.getWallet().getScriptType().toString());
|
||||
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save Text File");
|
||||
fileChooser.setInitialFileName("signmessage.txt");
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
if(!file.getName().toLowerCase(Locale.ROOT).endsWith(".txt")) {
|
||||
file = new File(file.getAbsolutePath() + ".txt");
|
||||
}
|
||||
|
||||
try(BufferedWriter writer = new BufferedWriter(new FileWriter(file, StandardCharsets.UTF_8))) {
|
||||
writer.write(joiner.toString());
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving signing message", e);
|
||||
AppServices.showErrorDialog("Error saving signing message", "Cannot write to " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void exportBip322File() {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
PSBT psbt = buildBip322Psbt(signingWallet);
|
||||
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save PSBT File");
|
||||
fileChooser.setInitialFileName("bip322-signmessage.psbt");
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
try(OutputStream os = new FileOutputStream(file)) {
|
||||
os.write(psbt.getForExport().serialize());
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving BIP-322 PSBT", e);
|
||||
AppServices.showErrorDialog("Error saving PSBT", "Cannot write to " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void importFile() {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Signed File");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("Text Files", "*.txt"),
|
||||
new FileChooser.ExtensionFilter("PSBT Files", "*.psbt")
|
||||
);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
|
||||
if(file != null) {
|
||||
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt") || isBip322()) {
|
||||
if(walletNode == null) {
|
||||
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
byte[] psbtBytes = Files.readAllBytes(file.toPath());
|
||||
PSBT signedPsbt = new PSBT(psbtBytes, false);
|
||||
String sig = extractBip322Signature(signedPsbt);
|
||||
if(sig != null) {
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
}
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt")) {
|
||||
log.error("Error loading signed PSBT", e);
|
||||
AppServices.showErrorDialog("Error loading signed PSBT", e.getMessage());
|
||||
return;
|
||||
}
|
||||
//Fall through to text handling for non-.psbt files
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
String content = Files.readString(file.toPath(), StandardCharsets.UTF_8);
|
||||
Matcher matcher = signedMessagePattern.matcher(content);
|
||||
if(matcher.matches()) {
|
||||
String signedMessage = matcher.group(1);
|
||||
String signedAddress = matcher.group(2);
|
||||
String signedSignature = matcher.group(3);
|
||||
|
||||
if(!message.getText().isEmpty() && !signedMessage.trim().equals(message.getText().trim().replaceAll("\r*\n*", ""))) {
|
||||
AppServices.showErrorDialog("Incorrect Message", "The file contained a different message of:\n\n" + signedMessage);
|
||||
return;
|
||||
} else if(!signedAddress.trim().equals(address.getText().trim())) {
|
||||
AppServices.showErrorDialog("Incorrect Address", "The file contained a different address of:\n\n" + signedAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
message.setText(signedMessage);
|
||||
signature.setText(signedSignature);
|
||||
} else {
|
||||
signature.setText(content);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Error loading signed message", e);
|
||||
AppServices.showErrorDialog("Error loading signed message", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Glyph getSignGlyph() {
|
||||
if(wallet != null) {
|
||||
if(wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
@@ -539,4 +765,37 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
decryptWalletService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageSignDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
SplitMenuButton signByButton = new SplitMenuButton();
|
||||
signByButton.setText("Sign by QR");
|
||||
signByButton.setDisable(wallet == null);
|
||||
signByButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
|
||||
signByButton.setGraphicTextGap(5);
|
||||
signByButton.setOnAction(event -> {
|
||||
showQr();
|
||||
});
|
||||
MenuItem exportFile = new MenuItem("Sign by File...");
|
||||
exportFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_EXPORT)));
|
||||
exportFile.setOnAction(event -> {
|
||||
exportFile();
|
||||
});
|
||||
MenuItem importFile = new MenuItem("Load Signed File...");
|
||||
importFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_IMPORT)));
|
||||
importFile.setOnAction(event -> {
|
||||
importFile();
|
||||
});
|
||||
signByButton.getItems().addAll(exportFile, importFile);
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(signByButton, buttonData);
|
||||
|
||||
return signByButton;
|
||||
}
|
||||
|
||||
return super.createButton(buttonType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@@ -49,8 +50,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("grid.css").toExternalForm());
|
||||
dialogPane.setHeaderText("Load a Border Wallets PDF, or generate a grid from a BIP39 seed.\nThen select 11 or 23 words in a pattern on the grid.\nThe order of selection is important!");
|
||||
javafx.scene.image.Image image = new Image("/image/border-wallets.png");
|
||||
dialogPane.setGraphic(new ImageView(image));
|
||||
dialogPane.setGraphic(new DialogImage(DialogImage.Type.BORDERWALLETS));
|
||||
|
||||
String[][] emptyWordGrid = new String[128][GRID_COLUMN_COUNT];
|
||||
Grid grid = getGrid(emptyWordGrid);
|
||||
@@ -256,7 +256,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open PDF");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("All Files", OsType.getCurrent().equals(OsType.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("PDF", "*.pdf")
|
||||
);
|
||||
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
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.GridPane;
|
||||
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 {
|
||||
private final DeterministicSeed.Type type;
|
||||
|
||||
public MnemonicKeystoreDisplayPane(Keystore keystore) {
|
||||
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", WalletModel.SEED);
|
||||
showHideLink.setVisible(false);
|
||||
buttonBox.getChildren().clear();
|
||||
this.type = keystore.getSeed().getType();
|
||||
|
||||
showWordList(keystore.getSeed());
|
||||
}
|
||||
@@ -28,11 +31,9 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords / 3);
|
||||
wordsPane = new GridPane();
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
|
||||
List<String> words = new ArrayList<>();
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
@@ -43,13 +44,20 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider()));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
|
||||
}
|
||||
wordsPane.getChildren().addAll(wordEntries);
|
||||
|
||||
int numCols = 3;
|
||||
int numRows = Math.ceilDiv(numWords, numCols);
|
||||
for(int i = 0; i < wordEntries.size(); i++) {
|
||||
int col = i / numRows;
|
||||
int row = i % numRows;
|
||||
wordsPane.add(wordEntries.get(i), col, row);
|
||||
}
|
||||
|
||||
vBox.getChildren().add(wordsPane);
|
||||
|
||||
@@ -57,4 +65,9 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
stackPane.getChildren().add(vBox);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WordlistProvider getWordlistProvider() {
|
||||
return getWordListProvider(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class MnemonicKeystoreEntryPane extends MnemonicKeystorePane {
|
||||
private boolean generated;
|
||||
|
||||
public MnemonicKeystoreEntryPane(String name, int numWords) {
|
||||
super(name, "Enter seed words", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
super(name, "Enter seed words", "", WalletModel.SEED);
|
||||
showHideLink.setVisible(false);
|
||||
buttonBox.getChildren().clear();
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
private List<String> generatedMnemonicCode;
|
||||
|
||||
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer, KeyDerivation defaultDerivation) {
|
||||
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
@@ -58,7 +58,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
importButton.getStyleClass().add("default-button");
|
||||
setDefaultButton(importButton);
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(getDefaultDerivation(), false);
|
||||
@@ -141,7 +141,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType().getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
|
||||
@@ -256,7 +256,7 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
|
||||
importButton.setDisable(true);
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
|
||||
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, wordEntriesProperty.get(), passphraseProperty.get());
|
||||
if(!dryrun) {
|
||||
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
|
||||
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
|
||||
|
||||
@@ -2,8 +2,11 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.Slip39MnemonicCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
@@ -12,20 +15,15 @@ import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.control.textfield.AutoCompletionBinding;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
@@ -41,7 +39,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
private static final Logger log = LoggerFactory.getLogger(MnemonicKeystorePane.class);
|
||||
|
||||
protected SplitMenuButton enterMnemonicButton;
|
||||
protected TilePane wordsPane;
|
||||
protected GridPane wordsPane;
|
||||
protected Label validLabel;
|
||||
protected Label invalidLabel;
|
||||
|
||||
@@ -49,8 +47,8 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
protected final SimpleStringProperty passphraseProperty = new SimpleStringProperty("");
|
||||
protected IntegerProperty defaultWordSizeProperty;
|
||||
|
||||
public MnemonicKeystorePane(String title, String description, String content, String imageUrl) {
|
||||
super(title, description, content, imageUrl);
|
||||
public MnemonicKeystorePane(String title, String description, String content, WalletModel walletModel) {
|
||||
super(title, description, content, walletModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,23 +109,9 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
wordEntry.getEditor().setText(words.get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
} else {
|
||||
ScheduledService<Void> service = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
service.setDelay(Duration.millis(500));
|
||||
service.setOnSucceeded(event1 -> {
|
||||
service.cancel();
|
||||
AppServices.runAfterDelay(500, () -> {
|
||||
Platform.runLater(() -> wordEntry.getEditor().requestFocus());
|
||||
});
|
||||
service.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +137,10 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
protected void showWordList(DeterministicSeed seed) {
|
||||
List<String> words = seed.getMnemonicCode();
|
||||
showWordList(words);
|
||||
}
|
||||
|
||||
protected void showWordList(List<String> words) {
|
||||
setContent(getMnemonicWordsEntry(words.size(), true, true));
|
||||
setExpanded(true);
|
||||
|
||||
@@ -174,11 +162,9 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords/3);
|
||||
wordsPane = new GridPane();
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
|
||||
List<String> words = new ArrayList<>();
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
@@ -189,13 +175,20 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider()));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
wordEntries.get(i).setNextField(wordEntries.get(i + 1).getEditor());
|
||||
}
|
||||
wordsPane.getChildren().addAll(wordEntries);
|
||||
|
||||
int numCols = 3;
|
||||
int numRows = Math.ceilDiv(numWords, numCols);
|
||||
for(int i = 0; i < wordEntries.size(); i++) {
|
||||
int col = i / numRows;
|
||||
int row = i % numRows;
|
||||
wordsPane.add(wordEntries.get(i), col, row);
|
||||
}
|
||||
|
||||
vBox.getChildren().add(wordsPane);
|
||||
|
||||
@@ -215,7 +208,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
buttonPane.getChildren().add(leftBox);
|
||||
AnchorPane.setLeftAnchor(leftBox, 0.0);
|
||||
|
||||
validLabel = new Label("Valid checksum", getValidGlyph());
|
||||
validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph());
|
||||
validLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
validLabel.setGraphicTextGap(5.0);
|
||||
validLabel.managedProperty().bind(validLabel.visibleProperty());
|
||||
@@ -224,7 +217,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
AnchorPane.setTopAnchor(validLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(validLabel, 0.0);
|
||||
|
||||
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
|
||||
invalidLabel = new Label("Invalid checksum", GlyphUtils.getInvalidGlyph());
|
||||
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
invalidLabel.setGraphicTextGap(5.0);
|
||||
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
|
||||
@@ -242,7 +235,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
empty = false;
|
||||
}
|
||||
|
||||
if(!WordEntry.isValid(word)) {
|
||||
if(!getWordlistProvider().isValid(word)) {
|
||||
validWords = false;
|
||||
}
|
||||
}
|
||||
@@ -278,13 +271,20 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
//nothing by default
|
||||
}
|
||||
|
||||
protected WordlistProvider getWordlistProvider() {
|
||||
return getWordListProvider(DeterministicSeed.Type.BIP39);
|
||||
}
|
||||
|
||||
protected WordlistProvider getWordListProvider(DeterministicSeed.Type type) {
|
||||
return type == DeterministicSeed.Type.SLIP39 ? new Slip39WordlistProvider() : new Bip39WordlistProvider();
|
||||
}
|
||||
|
||||
protected static class WordEntry extends HBox {
|
||||
private static List<String> wordList;
|
||||
private final TextField wordField;
|
||||
private WordEntry nextEntry;
|
||||
private TextField nextField;
|
||||
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList, WordlistProvider wordlistProvider) {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
@@ -302,7 +302,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
for(int i = 0; i < words.length; i++) {
|
||||
String word = words[i];
|
||||
if(entry.nextField != null) {
|
||||
if(i == words.length - 2 && isValid(word)) {
|
||||
if(i == words.length - 2 && wordlistProvider.isValid(word)) {
|
||||
label.requestFocus();
|
||||
} else {
|
||||
entry.nextField.requestFocus();
|
||||
@@ -321,6 +321,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
}
|
||||
};
|
||||
wordField.setMaxWidth(100);
|
||||
wordField.setAccessibleText("Word " + (wordNumber + 1));
|
||||
TextFormatter<?> formatter = new TextFormatter<>((TextFormatter.Change change) -> {
|
||||
String text = change.getText();
|
||||
// if text was added, fix the text to fit the requirements
|
||||
@@ -335,8 +336,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
});
|
||||
wordField.setTextFormatter(formatter);
|
||||
|
||||
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList, wordNumber, wordEntryList));
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordlistProvider, wordNumber, wordEntryList));
|
||||
autoCompletionBinding.setDelay(50);
|
||||
autoCompletionBinding.setOnAutoCompleted(event -> {
|
||||
if(nextField != null) {
|
||||
@@ -357,7 +357,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(wordField, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordList.contains(newValue))
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordlistProvider.isValid(newValue))
|
||||
));
|
||||
|
||||
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
@@ -378,28 +378,24 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
public void setNextField(TextField field) {
|
||||
this.nextField = field;
|
||||
}
|
||||
|
||||
public static boolean isValid(String word) {
|
||||
return wordList.contains(word);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
|
||||
private final List<String> wordList;
|
||||
private final WordlistProvider wordlistProvider;
|
||||
private final int wordNumber;
|
||||
private final ObservableList<String> wordEntryList;
|
||||
|
||||
public WordlistSuggestionProvider(List<String> wordList, int wordNumber, ObservableList<String> wordEntryList) {
|
||||
this.wordList = wordList;
|
||||
public WordlistSuggestionProvider(WordlistProvider wordlistProvider, int wordNumber, ObservableList<String> wordEntryList) {
|
||||
this.wordlistProvider = wordlistProvider;
|
||||
this.wordNumber = wordNumber;
|
||||
this.wordEntryList = wordEntryList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
|
||||
if(wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
|
||||
if(wordlistProvider.supportsPossibleLastWords() && wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
|
||||
try {
|
||||
List<String> possibleLastWords = Bip39MnemonicCode.INSTANCE.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
|
||||
List<String> possibleLastWords = wordlistProvider.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText()));
|
||||
}
|
||||
@@ -412,7 +408,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
for(String word : wordList) {
|
||||
for(String word : wordlistProvider.getWordlist()) {
|
||||
if(word.startsWith(request.getUserText())) {
|
||||
suggestions.add(word);
|
||||
}
|
||||
@@ -424,7 +420,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
private boolean allPreviousWordsValid() {
|
||||
for(int i = 0; i < wordEntryList.size() - 1; i++) {
|
||||
if(!WordEntry.isValid(wordEntryList.get(i))) {
|
||||
if(!wordlistProvider.isValid(wordEntryList.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -485,17 +481,53 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
public static Glyph getValidGlyph() {
|
||||
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||
validGlyph.getStyleClass().add("success");
|
||||
validGlyph.setFontSize(12);
|
||||
return validGlyph;
|
||||
protected interface WordlistProvider {
|
||||
List<String> getWordlist();
|
||||
boolean isValid(String word);
|
||||
boolean supportsPossibleLastWords();
|
||||
List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException;
|
||||
}
|
||||
|
||||
public static Glyph getInvalidGlyph() {
|
||||
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
invalidGlyph.getStyleClass().add("failure");
|
||||
invalidGlyph.setFontSize(12);
|
||||
return invalidGlyph;
|
||||
private static class Bip39WordlistProvider implements WordlistProvider {
|
||||
@Override
|
||||
public List<String> getWordlist() {
|
||||
return Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
}
|
||||
|
||||
public boolean isValid(String word) {
|
||||
return getWordlist().contains(word);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPossibleLastWords() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException {
|
||||
return Bip39MnemonicCode.INSTANCE.getPossibleLastWords(previousWords);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Slip39WordlistProvider implements WordlistProvider {
|
||||
@Override
|
||||
public List<String> getWordlist() {
|
||||
return Slip39MnemonicCode.INSTANCE.getWordList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String word) {
|
||||
return getWordlist().contains(word);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPossibleLastWords() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleLastWords(List<String> previousWords) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.Share;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreMnemonicShareImport;
|
||||
import com.sparrowwallet.sparrow.io.Slip39;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected final Wallet wallet;
|
||||
private final KeystoreMnemonicShareImport importer;
|
||||
private final KeyDerivation defaultDerivation;
|
||||
private final List<List<String>> mnemonicShares = new ArrayList<>();
|
||||
|
||||
private SplitMenuButton importButton;
|
||||
|
||||
private Button calculateButton;
|
||||
private Button backButton;
|
||||
private Button nextButton;
|
||||
private int currentShare;
|
||||
|
||||
public MnemonicShareKeystoreImportPane(Wallet wallet, KeystoreMnemonicShareImport importer, KeyDerivation defaultDerivation) {
|
||||
super(importer.getName(), "Enter seed share", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
|
||||
createImportButton();
|
||||
buttonBox.getChildren().add(importButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
createEnterMnemonicButton();
|
||||
return enterMnemonicButton;
|
||||
}
|
||||
|
||||
private void createEnterMnemonicButton() {
|
||||
enterMnemonicButton = new SplitMenuButton();
|
||||
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
enterMnemonicButton.setText("Use 20 Words");
|
||||
defaultWordSizeProperty = new SimpleIntegerProperty(20);
|
||||
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
|
||||
enterMnemonicButton.setText("Use " + newValue + " Words");
|
||||
});
|
||||
enterMnemonicButton.setOnAction(event -> {
|
||||
resetShares();
|
||||
enterMnemonic(defaultWordSizeProperty.get());
|
||||
});
|
||||
int[] numberWords = new int[] {20, 33};
|
||||
for(int i = 0; i < numberWords.length; i++) {
|
||||
MenuItem item = new MenuItem("Use " + numberWords[i] + " Words");
|
||||
final int words = numberWords[i];
|
||||
item.setOnAction(event -> {
|
||||
resetShares();
|
||||
defaultWordSizeProperty.set(words);
|
||||
enterMnemonic(words);
|
||||
});
|
||||
enterMnemonicButton.getItems().add(item);
|
||||
}
|
||||
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
||||
}
|
||||
|
||||
protected List<Node> createRightButtons() {
|
||||
calculateButton = new Button("Create Keystore");
|
||||
calculateButton.setDefaultButton(true);
|
||||
calculateButton.setOnAction(event -> {
|
||||
prepareImport();
|
||||
});
|
||||
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
|
||||
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided shares"));
|
||||
calculateButton.setVisible(false);
|
||||
|
||||
backButton = new Button("Back");
|
||||
backButton.setOnAction(event -> {
|
||||
lastShare();
|
||||
});
|
||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
backButton.setTooltip(new Tooltip("Display the last share added"));
|
||||
backButton.setVisible(currentShare > 0);
|
||||
|
||||
nextButton = new Button("Next");
|
||||
nextButton.setOnAction(event -> {
|
||||
nextShare();
|
||||
});
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
nextButton.setTooltip(new Tooltip("Add the next share"));
|
||||
nextButton.visibleProperty().bind(calculateButton.visibleProperty().not());
|
||||
nextButton.setDefaultButton(true);
|
||||
nextButton.setDisable(true);
|
||||
|
||||
return List.of(backButton, nextButton, calculateButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enterMnemonic(int numWords) {
|
||||
super.enterMnemonic(numWords);
|
||||
setDescription("Enter existing share");
|
||||
}
|
||||
|
||||
private void resetShares() {
|
||||
currentShare = 0;
|
||||
mnemonicShares.clear();
|
||||
}
|
||||
|
||||
private void lastShare() {
|
||||
currentShare--;
|
||||
showWordList(mnemonicShares.get(currentShare));
|
||||
}
|
||||
|
||||
private void nextShare() {
|
||||
if(currentShare == mnemonicShares.size()) {
|
||||
mnemonicShares.add(wordEntriesProperty.get());
|
||||
} else {
|
||||
mnemonicShares.set(currentShare, wordEntriesProperty.get());
|
||||
}
|
||||
|
||||
currentShare++;
|
||||
|
||||
if(currentShare < mnemonicShares.size()) {
|
||||
showWordList(mnemonicShares.get(currentShare));
|
||||
} else {
|
||||
setContent(getMnemonicWordsEntry(defaultWordSizeProperty.get(), true, true));
|
||||
}
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
boolean validSet = false;
|
||||
boolean complete = false;
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
Share.fromMnemonic(String.join(" ", wordEntriesProperty.get()));
|
||||
validChecksum = true;
|
||||
|
||||
List<List<String>> existing = new ArrayList<>(mnemonicShares);
|
||||
if(currentShare >= mnemonicShares.size()) {
|
||||
existing.add(wordEntriesProperty.get());
|
||||
}
|
||||
|
||||
importer.getKeystore(wallet.getPolicyType(), wallet.getScriptType().getDefaultDerivation(), existing, passphraseProperty.get());
|
||||
validSet = true;
|
||||
complete = true;
|
||||
} catch(MnemonicException e) {
|
||||
invalidLabel.setText(e.getTitle());
|
||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
||||
} catch(Slip39.Slip39ProgressException e) {
|
||||
validSet = true;
|
||||
invalidLabel.setText(e.getTitle());
|
||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException mnemonicException) {
|
||||
invalidLabel.setText(mnemonicException.getTitle());
|
||||
invalidLabel.setTooltip(new Tooltip(mnemonicException.getMessage()));
|
||||
} else {
|
||||
invalidLabel.setText("Import Error");
|
||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
calculateButton.setVisible(complete);
|
||||
backButton.setVisible(currentShare > 0 && !complete);
|
||||
nextButton.setDisable(!validChecksum || !validSet);
|
||||
validLabel.setVisible(complete);
|
||||
validLabel.setText(mnemonicShares.isEmpty() ? "Valid checksum" : "Completed share set");
|
||||
invalidLabel.setVisible(!complete && !empty);
|
||||
invalidLabel.setGraphic(validChecksum && validSet ? getIncompleteGlyph() : GlyphUtils.getFailureGlyph());
|
||||
}
|
||||
|
||||
private void createImportButton() {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
setDefaultButton(importButton);
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(getDefaultDerivation(), false);
|
||||
});
|
||||
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(derivation, false);
|
||||
});
|
||||
importButton.getItems().add(item);
|
||||
}
|
||||
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
importButton.setVisible(false);
|
||||
}
|
||||
|
||||
private List<ChildNumber> getDefaultDerivation() {
|
||||
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
private void prepareImport() {
|
||||
nextShare();
|
||||
backButton.setVisible(false);
|
||||
|
||||
if(importKeystore(wallet.getScriptType().getDefaultDerivation(), true)) {
|
||||
setExpanded(true);
|
||||
enterMnemonicButton.setVisible(false);
|
||||
importButton.setVisible(true);
|
||||
importButton.setDisable(false);
|
||||
setDescription("Ready to import");
|
||||
showHideLink.setText("Show Derivation...");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getDerivationEntry(getDefaultDerivation()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
|
||||
importButton.setDisable(true);
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(wallet.getPolicyType(), derivation, mnemonicShares, passphraseProperty.get());
|
||||
if(!dryrun) {
|
||||
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
|
||||
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
|
||||
keystorePassphraseDialog.initOwner(this.getScene().getWindow());
|
||||
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
|
||||
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
|
||||
throw new ImportException("Re-entered passphrase did not match");
|
||||
}
|
||||
}
|
||||
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
return true;
|
||||
} catch (ImportException e) {
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) {
|
||||
errorMessage = "Invalid word list - checksum incorrect";
|
||||
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Import Error", errorMessage + ".");
|
||||
importButton.setDisable(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Node getDerivationEntry(List<ChildNumber> derivation) {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(derivationField, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
|
||||
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
|
||||
importDerivationButton.setDisable(true);
|
||||
importDerivationButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
|
||||
importKeystore(importDerivation, false);
|
||||
});
|
||||
|
||||
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().add(derivationField);
|
||||
contentBox.getChildren().add(importDerivationButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
public static Glyph getIncompleteGlyph() {
|
||||
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PLUS_CIRCLE);
|
||||
warningGlyph.getStyleClass().add("warn-icon");
|
||||
warningGlyph.setFontSize(12);
|
||||
return warningGlyph;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WordlistProvider getWordlistProvider() {
|
||||
return getWordListProvider(DeterministicSeed.Type.SLIP39);
|
||||
}
|
||||
}
|
||||
+49
-27
@@ -41,7 +41,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
private Button importButton;
|
||||
|
||||
public MnemonicWalletKeystoreImportPane(KeystoreMnemonicImport importer) {
|
||||
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
super(importer.getName(), "Seed import", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected List<Node> createRightButtons() {
|
||||
discoverButton = new Button("Discover Wallet");
|
||||
discoverButton.setDisable(true);
|
||||
discoverButton.setDefaultButton(true);
|
||||
discoverButton.setDefaultButton(AppServices.onlineProperty().get());
|
||||
discoverButton.managedProperty().bind(discoverButton.visibleProperty());
|
||||
discoverButton.setOnAction(event -> {
|
||||
discoverWallet();
|
||||
@@ -66,6 +66,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
|
||||
importButton = new Button("Import Wallet");
|
||||
importButton.setDisable(true);
|
||||
importButton.setDefaultButton(!AppServices.onlineProperty().get());
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
importButton.visibleProperty().bind(discoverButton.visibleProperty().not());
|
||||
importButton.setOnAction(event -> {
|
||||
@@ -80,7 +81,7 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
importer.getKeystore(ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
importer.getKeystore(PolicyType.SINGLE_HD, ScriptType.P2WPKH.getDefaultDerivation(), wordEntriesProperty.get(), passphraseProperty.get());
|
||||
validChecksum = true;
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicTypeException) {
|
||||
@@ -107,14 +108,14 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
discoverButton.setGraphic(progressIndicator);
|
||||
List<Wallet> wallets = new ArrayList<>();
|
||||
|
||||
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
|
||||
List<List<ChildNumber>> derivations = ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD).stream().map(ScriptType::getDefaultDerivation).collect(Collectors.toList());
|
||||
derivations.add(List.of(new ChildNumber(0, true)));
|
||||
derivations.add(ScriptType.P2PKH.getDefaultDerivation(1)); //Bisq segwit misderivation
|
||||
|
||||
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE)) {
|
||||
for(ScriptType scriptType : ScriptType.getScriptTypesForPolicyType(PolicyType.SINGLE_HD)) {
|
||||
for(List<ChildNumber> derivation : derivations) {
|
||||
try {
|
||||
Wallet wallet = getWallet(scriptType, derivation);
|
||||
Wallet wallet = getWallet(PolicyType.SINGLE_HD, scriptType, derivation);
|
||||
wallets.add(wallet);
|
||||
} catch(ImportException e) {
|
||||
String errorMessage = e.getMessage();
|
||||
@@ -133,15 +134,21 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
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()));
|
||||
Optional<List<Wallet>> optWallets = walletDiscoveryService.getValue();
|
||||
if(optWallets.isPresent()) {
|
||||
List<Wallet> discoveredWallets = optWallets.get();
|
||||
if(discoveredWallets.size() > 1) {
|
||||
for(Wallet wallet : discoveredWallets) {
|
||||
wallet.setName(wallet.getKeystores().getFirst().getLabel() + " " + wallet.getScriptType().getDescription());
|
||||
}
|
||||
}
|
||||
EventManager.get().post(new WalletImportEvent(discoveredWallets));
|
||||
} else {
|
||||
discoverButton.setDisable(false);
|
||||
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("No existing wallet found",
|
||||
Config.get().getServerType() == ServerType.BITCOIN_CORE ? "The configured server type is Bitcoin Core, which does not support wallet discovery.\n\n" +
|
||||
"You can however import this wallet and scan the blockchain by supplying a start date. Do you want to import this wallet?" :
|
||||
"Could not find a wallet with existing transactions using this mnemonic. Import this wallet anyway?", ButtonType.NO, ButtonType.YES);
|
||||
"Could not find an HD 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);
|
||||
@@ -156,52 +163,61 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
walletDiscoveryService.start();
|
||||
}
|
||||
|
||||
private Wallet getWallet(ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
|
||||
private Wallet getWallet(PolicyType policyType, ScriptType scriptType, List<ChildNumber> derivation) throws ImportException {
|
||||
Wallet wallet = new Wallet("");
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setPolicyType(policyType);
|
||||
wallet.setScriptType(scriptType);
|
||||
Keystore keystore = importer.getKeystore(derivation, wordEntriesProperty.get(), passphraseProperty.get());
|
||||
Keystore keystore = importer.getKeystore(policyType, derivation, wordEntriesProperty.get(), passphraseProperty.get());
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, scriptType, wallet.getKeystores(), 1));
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(policyType, scriptType, wallet.getKeystores(), 1));
|
||||
return wallet;
|
||||
}
|
||||
|
||||
private Node getScriptTypeEntry() {
|
||||
Label label = new Label("Script Type:");
|
||||
Label label = new Label("Type:");
|
||||
|
||||
List<PolicyAndScriptType> types = new ArrayList<>();
|
||||
for(PolicyType policyType : List.of(PolicyType.SINGLE_HD, PolicyType.SINGLE_SP)) {
|
||||
for(ScriptType scriptType : ScriptType.getAddressableScriptTypes(policyType)) {
|
||||
types.add(new PolicyAndScriptType(policyType, scriptType));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
ComboBox<PolicyAndScriptType> comboBox = new ComboBox<>(FXCollections.observableArrayList(types));
|
||||
PolicyAndScriptType defaultType = new PolicyAndScriptType(PolicyType.SINGLE_HD, ScriptType.P2WPKH);
|
||||
if(types.contains(defaultType)) {
|
||||
comboBox.setValue(defaultType);
|
||||
}
|
||||
scriptTypeComboBox.setConverter(new StringConverter<>() {
|
||||
comboBox.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(ScriptType scriptType) {
|
||||
return scriptType == null ? "" : scriptType.getDescription();
|
||||
public String toString(PolicyAndScriptType type) {
|
||||
return type == null ? "" : type.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScriptType fromString(String string) {
|
||||
public PolicyAndScriptType fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
scriptTypeComboBox.setMaxWidth(170);
|
||||
comboBox.setMaxWidth(220);
|
||||
|
||||
HelpLabel helpLabel = new HelpLabel();
|
||||
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
fieldBox.getChildren().addAll(scriptTypeComboBox, helpLabel);
|
||||
helpLabel.setHelpText("Native Segwit is usually the best choice for new wallets.\nTaproot is a new type useful for specific needs.\nTaproot Silent Payments creates a silent payment wallet.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are importing.");
|
||||
fieldBox.getChildren().addAll(comboBox, helpLabel);
|
||||
|
||||
Region region = new Region();
|
||||
HBox.setHgrow(region, Priority.SOMETIMES);
|
||||
|
||||
Button importMnemonicButton = new Button("Import");
|
||||
importMnemonicButton.setDefaultButton(true);
|
||||
importMnemonicButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
try {
|
||||
ScriptType scriptType = scriptTypeComboBox.getValue();
|
||||
Wallet wallet = getWallet(scriptType, scriptType.getDefaultDerivation());
|
||||
PolicyAndScriptType type = comboBox.getValue();
|
||||
Wallet wallet = getWallet(type.policyType(), type.scriptType(), type.scriptType().getDefaultDerivation());
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} catch(ImportException e) {
|
||||
log.error("Error importing mnemonic", e);
|
||||
@@ -223,4 +239,10 @@ public class MnemonicWalletKeystoreImportPane extends MnemonicKeystorePane {
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
protected record PolicyAndScriptType(PolicyType policyType, ScriptType scriptType) {
|
||||
public String getDescription() {
|
||||
return scriptType.getDescription() + (policyType == PolicyType.SINGLE_SP ? " SP" : " HD");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import javafx.scene.control.TreeTableCell;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
public class NumberCell extends TreeTableCell<Entry, Number> {
|
||||
public NumberCell() {
|
||||
super();
|
||||
getStyleClass().add("number-cell");
|
||||
if(Platform.getCurrent() == Platform.OSX) {
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
getStyleClass().add("number-field");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -124,8 +124,11 @@ public class PayNymAvatar extends StackPane {
|
||||
log.debug("Requesting PayNym avatar from " + url);
|
||||
}
|
||||
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
|
||||
Image image = new Image(is, 150, 150, true, false);
|
||||
try(InputStream is = (proxy == null ? new URI(url).toURL().openStream() : new URI(url).toURL().openConnection(proxy).getInputStream())) {
|
||||
Image image = new Image(is, 150, 150, true, true);
|
||||
if(image.getException() != null) {
|
||||
throw image.getException();
|
||||
}
|
||||
paymentCodeCache.put(cacheId, image);
|
||||
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
|
||||
return image;
|
||||
|
||||
@@ -13,7 +13,11 @@ 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.psbt.PSBTProofException;
|
||||
import com.sparrowwallet.drongo.silentpayments.*;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
@@ -27,8 +31,6 @@ 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;
|
||||
@@ -61,10 +63,12 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
private final TextArea key;
|
||||
private final ComboBox<ScriptType> keyScriptType;
|
||||
private final CopyableLabel keyAddress;
|
||||
private final CopyableLabel keyUtxos;
|
||||
private final ComboBoxTextField toAddress;
|
||||
private final ComboBox<Wallet> toWallet;
|
||||
private final FeeRangeSlider feeRange;
|
||||
private final CopyableLabel feeRate;
|
||||
private SilentPaymentAddress silentPaymentAddress;
|
||||
|
||||
public PrivateKeySweepDialog(Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
@@ -72,14 +76,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
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);
|
||||
}
|
||||
dialogPane.setGraphic(new WalletModelImage(WalletModel.SEED));
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset fieldset = new Fieldset();
|
||||
@@ -115,7 +112,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
Field keyScriptTypeField = new Field();
|
||||
keyScriptTypeField.setText("Script Type:");
|
||||
keyScriptType = new ComboBox<>();
|
||||
keyScriptType.setItems(FXCollections.observableList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE)));
|
||||
keyScriptType.setItems(FXCollections.observableList(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD)));
|
||||
keyScriptTypeField.getInputs().add(keyScriptType);
|
||||
|
||||
keyScriptType.setConverter(new StringConverter<ScriptType>() {
|
||||
@@ -136,10 +133,17 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
keyAddress.getStyleClass().add("fixed-width");
|
||||
addressField.getInputs().add(keyAddress);
|
||||
|
||||
Field utxosField = new Field();
|
||||
utxosField.setText("UTXOs:");
|
||||
keyUtxos = new CopyableLabel();
|
||||
utxosField.getInputs().add(keyUtxos);
|
||||
|
||||
|
||||
Field toAddressField = new Field();
|
||||
toAddressField.setText("Sweep to:");
|
||||
toAddress = new ComboBoxTextField();
|
||||
toAddress.getStyleClass().add("fixed-width");
|
||||
toAddress.setSkin(new AddressTextFieldSkin(toAddress));
|
||||
toWallet = new ComboBox<>();
|
||||
toWallet.setItems(FXCollections.observableList(AppServices.get().getOpenWallets().keySet().stream()
|
||||
.filter(w -> !w.isWhirlpoolChildWallet() && !w.isBip47()).collect(Collectors.toList())));
|
||||
@@ -203,18 +207,31 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
});
|
||||
|
||||
toAddress.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
try {
|
||||
silentPaymentAddress = SilentPaymentAddress.from(newValue);
|
||||
} catch(Exception e) {
|
||||
silentPaymentAddress = null;
|
||||
}
|
||||
createButton.setDisable(!isValidKey() || !isValidToAddress());
|
||||
});
|
||||
|
||||
toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> {
|
||||
if(selectedWallet != null) {
|
||||
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
if(selectedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
toAddress.setText(selectedWallet.getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
|
||||
} else {
|
||||
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
keyScriptType.setValue(ScriptType.P2PKH);
|
||||
if(wallet != null) {
|
||||
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
toAddress.setText(wallet.getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
|
||||
} else {
|
||||
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));
|
||||
@@ -271,10 +288,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
}
|
||||
|
||||
private boolean isValidToAddress() {
|
||||
try {
|
||||
Address address = getToAddress();
|
||||
if(silentPaymentAddress != null) {
|
||||
return true;
|
||||
} catch (InvalidAddressException e) {
|
||||
}
|
||||
try {
|
||||
getToAddress();
|
||||
return true;
|
||||
} catch(InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -286,14 +306,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
private void setFromAddress() {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
ScriptType scriptType = keyScriptType.getValue();
|
||||
Address address = scriptType.getAddress(privateKey.getKey());
|
||||
Address address = scriptType.getAddress(PolicyType.SINGLE_HD, privateKey.getKey());
|
||||
keyAddress.setText(address.toString());
|
||||
}
|
||||
|
||||
private void setScriptTypes(boolean isValidKey) {
|
||||
boolean compressed = !isValidKey || getPrivateKey().getKey().isCompressed();
|
||||
if(compressed && !keyScriptType.getItems().equals(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE))) {
|
||||
keyScriptType.getItems().addAll(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE).stream().filter(s -> !keyScriptType.getItems().contains(s)).collect(Collectors.toList()));
|
||||
if(compressed && !keyScriptType.getItems().equals(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD))) {
|
||||
keyScriptType.getItems().addAll(ScriptType.getAddressableScriptTypes(PolicyType.SINGLE_HD).stream().filter(s -> !keyScriptType.getItems().contains(s)).collect(Collectors.toList()));
|
||||
} else if(!compressed && !keyScriptType.getItems().equals(List.of(ScriptType.P2PKH))) {
|
||||
keyScriptType.getSelectionModel().select(0);
|
||||
keyScriptType.getItems().removeIf(scriptType -> scriptType != ScriptType.P2PKH);
|
||||
@@ -345,8 +365,9 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
try {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
ScriptType scriptType = keyScriptType.getValue();
|
||||
Address fromAddress = scriptType.getAddress(privateKey.getKey());
|
||||
Address destAddress = getToAddress();
|
||||
Address fromAddress = scriptType.getAddress(PolicyType.SINGLE_HD, privateKey.getKey());
|
||||
Payment payment = silentPaymentAddress != null ? new SilentPayment(silentPaymentAddress, null, 0, true)
|
||||
: new Payment(getToAddress(), null, 0, true);
|
||||
|
||||
Date since = null;
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
@@ -355,12 +376,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
Optional<Date> optSince = addressScanDateDialog.showAndWait();
|
||||
if(optSince.isPresent()) {
|
||||
since = optSince.get();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress, since);
|
||||
addressUtxosService.setOnSucceeded(successEvent -> {
|
||||
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
|
||||
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), payment);
|
||||
});
|
||||
addressUtxosService.setOnFailed(failedEvent -> {
|
||||
Throwable rootCause = Throwables.getRootCause(failedEvent.getSource().getException());
|
||||
@@ -369,7 +392,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
});
|
||||
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", "/image/sparrow.png", addressUtxosService);
|
||||
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", new DialogImage(DialogImage.Type.SPARROW), addressUtxosService);
|
||||
serviceProgressDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
|
||||
}
|
||||
@@ -380,13 +403,14 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
}
|
||||
}
|
||||
|
||||
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Address destAddress) {
|
||||
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Payment payment) {
|
||||
Address destAddress = payment instanceof SilentPayment silentPayment ? computeSilentPaymentAddress(privKey, scriptType, txOutputs, silentPayment) : payment.getAddress();
|
||||
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));
|
||||
scriptType.addSpendingInput(PolicyType.SINGLE_HD, noFeeTransaction, txOutput, pubKey, TransactionSignature.dummy(scriptType == P2TR ? TransactionSignature.Type.SCHNORR : TransactionSignature.Type.ECDSA));
|
||||
total += txOutput.getValue();
|
||||
}
|
||||
|
||||
@@ -395,18 +419,24 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
|
||||
double feeRate = feeRange.getFeeRate();
|
||||
long fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate);
|
||||
if(feeRate == Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
if(feeRate == AppServices.getMinimumRelayFeeRate() && feeRate > 0d) {
|
||||
fee++;
|
||||
}
|
||||
|
||||
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
|
||||
if(total - fee <= dustThreshold) {
|
||||
feeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
|
||||
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1;
|
||||
feeRate = AppServices.getMinimumRelayFeeRate();
|
||||
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + (feeRate > 0d ? 1 : 0);
|
||||
|
||||
if(total - fee <= dustThreshold) {
|
||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||
return;
|
||||
} else {
|
||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds (" + total + " sats) for a transaction at this fee rate." +
|
||||
"\n\nContinue with a minimum fee rate transaction?", ButtonType.YES, ButtonType.NO);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.NO) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +469,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
psbtInput.setWitnessScript(txInput.getWitness().getWitnessScript());
|
||||
}
|
||||
|
||||
if(!psbtInput.sign(scriptType.getOutputKey(privKey))) {
|
||||
if(!psbtInput.sign(scriptType.getOutputKey(PolicyType.SINGLE_HD, privKey))) {
|
||||
AppServices.showErrorDialog("Failed to sign", "Failed to sign for transaction output " + utxoOutput.getHash() + ":" + utxoOutput.getIndex());
|
||||
return;
|
||||
}
|
||||
@@ -447,12 +477,39 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
|
||||
|
||||
Transaction finalizeTransaction = new Transaction();
|
||||
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
|
||||
TransactionInput finalizedTxInput = scriptType.addSpendingInput(PolicyType.SINGLE_HD, finalizeTransaction, utxoOutput, pubKey, signature);
|
||||
psbtInput.setFinalScriptSig(finalizedTxInput.getScriptSig());
|
||||
psbtInput.setFinalScriptWitness(finalizedTxInput.getWitness());
|
||||
}
|
||||
|
||||
setResult(psbt.extractTransaction());
|
||||
try {
|
||||
setResult(psbt.extractTransaction());
|
||||
} catch(PSBTProofException e) {
|
||||
AppServices.showErrorDialog("Invalid Silent Payments Transaction", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Address computeSilentPaymentAddress(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, SilentPayment silentPayment) {
|
||||
ECKey summedPrivateKey = scriptType.getOutputKey(PolicyType.SINGLE_HD, privKey);
|
||||
if(scriptType == P2TR && summedPrivateKey.hasOddYCoord()) {
|
||||
summedPrivateKey = summedPrivateKey.negatePrivate();
|
||||
}
|
||||
|
||||
Set<HashIndex> outpoints = new LinkedHashSet<>();
|
||||
for(TransactionOutput txOutput : txOutputs) {
|
||||
outpoints.add(new HashIndex(txOutput.getHash(), txOutput.getIndex()));
|
||||
}
|
||||
|
||||
try {
|
||||
SilentPaymentUtils.computeOutputAddresses(List.of(silentPayment), summedPrivateKey, outpoints);
|
||||
if(!silentPayment.isAddressComputed()) {
|
||||
throw new IllegalStateException("Failed to compute silent payment address");
|
||||
}
|
||||
|
||||
return silentPayment.getAddress();
|
||||
} catch(InvalidSilentPaymentException e) {
|
||||
throw new IllegalStateException("Failed to compute silent payment address", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Glyph getGlyph(FontAwesome5.Glyph glyphEnum) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.sparrowwallet.hummingbird.LegacyUREncoder;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
@@ -56,7 +57,11 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
|
||||
private final BBQR bbqr;
|
||||
private BBQREncoder bbqrEncoder;
|
||||
private boolean useBbqrEncoding;
|
||||
|
||||
private final String raw;
|
||||
private final boolean rawEncodable;
|
||||
|
||||
private QREncoding encoding = QREncoding.UR;
|
||||
|
||||
private final ImageView qrImageView;
|
||||
|
||||
@@ -71,14 +76,18 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
private static boolean initialDensityChange;
|
||||
|
||||
public QRDisplayDialog(String type, byte[] data, boolean addLegacyEncodingOption) throws UR.URException {
|
||||
this(UR.fromBytes(type, data), null, addLegacyEncodingOption, false, false);
|
||||
this(UR.fromBytes(type, data), null, addLegacyEncodingOption, false, QREncoding.UR);
|
||||
}
|
||||
|
||||
public QRDisplayDialog(UR ur) {
|
||||
this(ur, null, false, false, false);
|
||||
this(ur, null, false, false, QREncoding.UR);
|
||||
}
|
||||
|
||||
public QRDisplayDialog(UR ur, BBQR bbqr, boolean addLegacyEncodingOption, boolean addScanButton, boolean selectBbqrButton) {
|
||||
public QRDisplayDialog(UR ur, BBQR bbqr, boolean addLegacyEncodingOption, boolean addScanButton, QREncoding defaultEncoding) {
|
||||
this(ur, bbqr, null, addLegacyEncodingOption, addScanButton, defaultEncoding);
|
||||
}
|
||||
|
||||
public QRDisplayDialog(UR ur, BBQR bbqr, String raw, boolean addLegacyEncodingOption, boolean addScanButton, QREncoding defaultEncoding) {
|
||||
this.ur = ur;
|
||||
this.bbqr = bbqr;
|
||||
this.addLegacyEncodingOption = bbqr == null && addLegacyEncodingOption;
|
||||
@@ -87,11 +96,14 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
|
||||
if(bbqr != null) {
|
||||
this.bbqrEncoder = new BBQREncoder(bbqr.type(), DEFAULT_BBQR_ENCODING, bbqr.data(), Config.get().getQrDensity().getMaxBbqrFragmentLength(), 0);
|
||||
if(selectBbqrButton) {
|
||||
useBbqrEncoding = true;
|
||||
if(defaultEncoding == QREncoding.BBQR || Config.get().getQrEncoding() == QREncoding.BBQR) {
|
||||
encoding = QREncoding.BBQR;
|
||||
}
|
||||
}
|
||||
|
||||
this.raw = raw;
|
||||
this.rawEncodable = raw != null && getQrCode(raw, true) != null;
|
||||
|
||||
final DialogPane dialogPane = new QRDisplayDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
@@ -140,6 +152,11 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
dialogPane.getButtonTypes().add(scanButtonType);
|
||||
}
|
||||
|
||||
ButtonBar buttonBar = (ButtonBar)dialogPane.lookup(".button-bar");
|
||||
if(buttonBar != null) {
|
||||
buttonBar.setButtonOrder("L+B+C+O");
|
||||
}
|
||||
|
||||
dialogPane.setPrefWidth(40 + qrSize + 40);
|
||||
dialogPane.setPrefHeight(40 + qrSize + 85);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
@@ -155,8 +172,10 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
public QRDisplayDialog(String data, boolean addScanButton) {
|
||||
this.ur = null;
|
||||
this.bbqr = null;
|
||||
this.raw = data;
|
||||
this.urEncoder = null;
|
||||
this.bbqrEncoder = null;
|
||||
this.rawEncodable = true;
|
||||
|
||||
final DialogPane dialogPane = new QRDisplayDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
@@ -169,6 +188,11 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
qrImageView.setImage(getQrCode(data));
|
||||
|
||||
if(qrImageView.getImage() == null) {
|
||||
Label warning = new Label("Message is too long for display as a QR code");
|
||||
stackPane.getChildren().add(warning);
|
||||
}
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
|
||||
@@ -199,7 +223,9 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
|
||||
private boolean isSinglePart() {
|
||||
if(useBbqrEncoding) {
|
||||
if(encoding == QREncoding.RAW) {
|
||||
return true;
|
||||
} else if(encoding == QREncoding.BBQR) {
|
||||
return bbqrEncoder.isSinglePart();
|
||||
} else if(!useLegacyEncoding) {
|
||||
return urEncoder.isSinglePart();
|
||||
@@ -209,7 +235,9 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
|
||||
private void nextPart() {
|
||||
if(useBbqrEncoding) {
|
||||
if(encoding == QREncoding.RAW) {
|
||||
currentPart = raw;
|
||||
} else if(encoding == QREncoding.BBQR) {
|
||||
String fragment = bbqrEncoder.nextPart();
|
||||
currentPart = fragment.toUpperCase(Locale.ROOT);
|
||||
} else if(!useLegacyEncoding) {
|
||||
@@ -225,6 +253,10 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
|
||||
protected Image getQrCode(String fragment) {
|
||||
return getQrCode(fragment, false);
|
||||
}
|
||||
|
||||
protected Image getQrCode(String fragment, boolean trial) {
|
||||
try {
|
||||
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
||||
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, qrSize, qrSize, Map.of(EncodeHintType.MARGIN, "2"));
|
||||
@@ -235,7 +267,9 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
|
||||
return new Image(bais);
|
||||
} catch(Exception e) {
|
||||
log.error("Error generating QR", e);
|
||||
if(!trial) {
|
||||
log.error("Error generating QR", e);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -259,18 +293,13 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUseBbqrEncoding() {
|
||||
return useBbqrEncoding;
|
||||
public QREncoding getEncoding() {
|
||||
return encoding;
|
||||
}
|
||||
|
||||
private void setUseBbqrEncoding(boolean useBbqrEncoding) {
|
||||
if(useBbqrEncoding) {
|
||||
this.useBbqrEncoding = true;
|
||||
restartAnimation();
|
||||
} else {
|
||||
this.useBbqrEncoding = false;
|
||||
restartAnimation();
|
||||
}
|
||||
private void setEncoding(QREncoding encoding) {
|
||||
this.encoding = encoding;
|
||||
restartAnimation();
|
||||
}
|
||||
|
||||
private void changeQRDensity() {
|
||||
@@ -367,18 +396,29 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
|
||||
return scanButton;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.BACK_PREVIOUS) {
|
||||
ToggleButton bbqr = new ToggleButton(buttonType.getText());
|
||||
bbqr.setGraphicTextGap(5);
|
||||
bbqr.setGraphic(getGlyph(FontAwesome5.Glyph.QRCODE));
|
||||
bbqr.setSelected(useBbqrEncoding);
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(bbqr, buttonData);
|
||||
ComboBox<QREncoding> encodingComboBox = new ComboBox<>();
|
||||
if(ur != null) {
|
||||
encodingComboBox.getItems().add(QREncoding.UR);
|
||||
}
|
||||
if(bbqr != null) {
|
||||
encodingComboBox.getItems().add(QREncoding.BBQR);
|
||||
}
|
||||
if(raw != null) {
|
||||
encodingComboBox.getItems().add(QREncoding.RAW);
|
||||
}
|
||||
|
||||
bbqr.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
setUseBbqrEncoding(newValue);
|
||||
encodingComboBox.setCellFactory(_ -> new QREncodingListCell());
|
||||
encodingComboBox.setButtonCell(new QREncodingButtonCell());
|
||||
encodingComboBox.setValue(encoding);
|
||||
encodingComboBox.setOnAction(_ -> {
|
||||
setEncoding(encodingComboBox.getValue());
|
||||
Config.get().setQrEncoding(encodingComboBox.getValue());
|
||||
});
|
||||
|
||||
return bbqr;
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(encodingComboBox, buttonData);
|
||||
|
||||
return encodingComboBox;
|
||||
}
|
||||
|
||||
return super.createButton(buttonType);
|
||||
@@ -393,7 +433,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
|
||||
private void updateDensityButton(Button density) {
|
||||
density.setText(Config.get().getQrDensity() == QRDensity.NORMAL ? "Decrease Density" : "Increase Density");
|
||||
density.setText(Config.get().getQrDensity() == QRDensity.NORMAL ? "Less Dense" : "More Dense");
|
||||
if(Config.get().getQrDensity() == QRDensity.NORMAL) {
|
||||
density.setGraphic(getGlyph(FontAwesome5.Glyph.MAGNIFYING_GLASS_PLUS));
|
||||
} else {
|
||||
@@ -407,4 +447,38 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
}
|
||||
|
||||
private class QREncodingListCell extends ListCell<QREncoding> {
|
||||
@Override
|
||||
protected void updateItem(QREncoding item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if(empty || item == null) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
setDisable(false);
|
||||
setOpacity(1.0);
|
||||
} else {
|
||||
setText(item.getName() + " Encoding");
|
||||
setGraphic(item.getSVGImage());
|
||||
setGraphicTextGap(8.0d);
|
||||
setDisable(item == QREncoding.RAW && !rawEncodable);
|
||||
setOpacity(isDisabled() ? 0.5 : 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class QREncodingButtonCell extends ListCell<QREncoding> {
|
||||
@Override
|
||||
protected void updateItem(QREncoding item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if(item == null || empty) {
|
||||
setText("");
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(item.getName());
|
||||
setGraphic(item.getSVGImage());
|
||||
setGraphicTextGap(8.0d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
|
||||
public enum QREncoding {
|
||||
UR("UR"), BBQR("BBQr"), RAW("Raw");
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(QREncoding.class);
|
||||
|
||||
QREncoding(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
private final String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Node getSVGImage() {
|
||||
try {
|
||||
URL url = AppServices.class.getResource("/image/qrencoding/" + getName().toLowerCase(Locale.ROOT) + "-icon" + (Config.get().getTheme() == Theme.DARK ? "-invert" : "") + ".svg");
|
||||
if(url != null) {
|
||||
return SVGLoader.load(url);
|
||||
} else {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE);
|
||||
glyph.setFontSize(12);
|
||||
glyph.setPadding(new Insets(0, 2, 0, 0));
|
||||
return glyph;
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Could not load QR encoding source image for " + name);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.github.sarxos.webcam.*;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||
import com.sparrowwallet.drongo.address.P2SHAddress;
|
||||
@@ -28,7 +25,8 @@ import com.sparrowwallet.hummingbird.URDecoder;
|
||||
import com.sparrowwallet.hummingbird.registry.pathcomponent.IndexPathComponent;
|
||||
import com.sparrowwallet.hummingbird.registry.pathcomponent.PathComponent;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WebcamResolutionChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRDecoder;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRException;
|
||||
@@ -40,17 +38,20 @@ import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Borders;
|
||||
import org.openpnp.capture.CaptureDevice;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
@@ -77,107 +78,141 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
private static final Pattern PART_PATTERN = Pattern.compile("p(\\d+)of(\\d+) (.+)");
|
||||
|
||||
private static final int SCAN_PERIOD_MILLIS = 100;
|
||||
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.VGA);
|
||||
private final ObjectProperty<CaptureDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
||||
private final ObjectProperty<WebcamResolution> webcamResolutionProperty = new SimpleObjectProperty<>(WebcamResolution.HD);
|
||||
|
||||
private final DoubleProperty percentComplete = new SimpleDoubleProperty(0.0);
|
||||
|
||||
private final ObjectProperty<WebcamDevice> webcamDeviceProperty = new SimpleObjectProperty<>();
|
||||
private final ObservableList<CaptureDevice> foundDevices = FXCollections.observableList(new ArrayList<>());
|
||||
private final ObservableList<WebcamResolution> availableResolutions = FXCollections.observableList(new ArrayList<>());
|
||||
private boolean postOpenUpdate;
|
||||
|
||||
public QRScanDialog() {
|
||||
this.urDecoder = new URDecoder();
|
||||
this.legacyUrDecoder = new LegacyURDecoder();
|
||||
this.bbqrDecoder = new BBQRDecoder();
|
||||
|
||||
if(Config.get().isHdCapture()) {
|
||||
webcamResolutionProperty.set(WebcamResolution.HD);
|
||||
if(Config.get().getWebcamResolution() != null) {
|
||||
webcamResolutionProperty.set(Config.get().getWebcamResolution());
|
||||
}
|
||||
|
||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null, new QRScanListener(), new ScanDelayCalculator());
|
||||
this.webcamService = new WebcamService(webcamResolutionProperty.get(), null);
|
||||
webcamService.setPeriod(Duration.millis(SCAN_PERIOD_MILLIS));
|
||||
webcamService.setRestartOnFailure(false);
|
||||
WebcamView webcamView = new WebcamView(webcamService);
|
||||
|
||||
final DialogPane dialogPane = new QRScanDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(webcamView.getView());
|
||||
Node wrappedView = Borders.wrap(stackPane).lineBorder().buildAll();
|
||||
WebcamView webcamView = new WebcamView(webcamService, Config.get().isMirrorCapture());
|
||||
|
||||
ProgressBar progressBar = new ProgressBar();
|
||||
progressBar.setMinHeight(20);
|
||||
progressBar.setPadding(new Insets(0, 10, 0, 10));
|
||||
progressBar.setPrefWidth(Integer.MAX_VALUE);
|
||||
progressBar.progressProperty().bind(percentComplete);
|
||||
webcamService.openingProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(percentComplete.get() <= 0.0) {
|
||||
Platform.runLater(() -> percentComplete.set(newValue ? 0.0 : -1.0));
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
if(Config.get().getWebcamDevice() != null && webcamDeviceProperty.get() == null) {
|
||||
for(WebcamDevice device : WebcamScanDriver.getFoundDevices()) {
|
||||
if(device.getName().equals(Config.get().getWebcamDevice())) {
|
||||
webcamDeviceProperty.set(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
VBox vBox = new VBox(20);
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(webcamView.getView());
|
||||
Node wrappedView = Borders.wrap(stackPane).lineBorder().buildAll();
|
||||
vBox.getChildren().addAll(wrappedView, progressBar);
|
||||
|
||||
dialogPane.setContent(vBox);
|
||||
|
||||
webcamService.openingProperty().addListener((_, _, opening) -> {
|
||||
if(percentComplete.get() <= 0.0) {
|
||||
Platform.runLater(() -> percentComplete.set(opening ? 0.0 : -1.0));
|
||||
}
|
||||
});
|
||||
|
||||
webcamService.openedProperty().addListener((_, _, opened) -> {
|
||||
if(opened) {
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
postOpenUpdate = true;
|
||||
List<CaptureDevice> newDevices = new ArrayList<>(webcamService.getAvailableDevices());
|
||||
newDevices.removeAll(foundDevices);
|
||||
foundDevices.addAll(newDevices);
|
||||
foundDevices.removeIf(device -> !webcamService.getDevices().contains(device));
|
||||
|
||||
if(webcamService.getDevice() != null) {
|
||||
for(CaptureDevice device : foundDevices) {
|
||||
if(device.equals(webcamService.getDevice())) {
|
||||
webcamDeviceProperty.set(device);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateList(availableResolutions, webcamService.getResolutions());
|
||||
webcamResolutionProperty.set(webcamService.getResolution());
|
||||
} finally {
|
||||
postOpenUpdate = false;
|
||||
}
|
||||
});
|
||||
} else if(webcamResolutionProperty.get() != null) {
|
||||
webcamService.setResolution(webcamResolutionProperty.get());
|
||||
webcamService.setDevice(webcamDeviceProperty.get());
|
||||
Platform.runLater(() -> {
|
||||
if(!webcamService.isRunning()) {
|
||||
webcamService.reset();
|
||||
webcamService.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
webcamService.resultProperty().addListener(new QRResultListener());
|
||||
webcamService.setOnFailed(failedEvent -> {
|
||||
Throwable exception = failedEvent.getSource().getException();
|
||||
|
||||
Throwable nested = exception;
|
||||
while(nested.getCause() != null) {
|
||||
nested = nested.getCause();
|
||||
}
|
||||
if(org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS &&
|
||||
nested.getMessage().startsWith("Library 'OpenIMAJGrabber' was not loaded successfully from file")) {
|
||||
exception = new WebcamDependencyException("Your system is missing a dependency required for the webcam. Follow the link below for more details.\n\n[https://sparrowwallet.com/docs/faq.html#your-system-is-missing-a-dependency-for-the-webcam]", exception);
|
||||
} else if(nested.getMessage().startsWith("Cannot start native grabber") && Config.get().getWebcamDevice() != null) {
|
||||
exception = new WebcamOpenException("Cannot open configured webcam " + Config.get().getWebcamDevice() + ", reverting to the default webcam");
|
||||
Config.get().setWebcamDevice(null);
|
||||
}
|
||||
|
||||
final Throwable result = exception;
|
||||
Platform.runLater(() -> setResult(new Result(result)));
|
||||
Throwable exception = Throwables.getRootCause(failedEvent.getSource().getException());
|
||||
Platform.runLater(() -> setResult(new Result(exception)));
|
||||
});
|
||||
webcamService.start();
|
||||
webcamResolutionProperty.addListener((observable, oldValue, newResolution) -> {
|
||||
|
||||
webcamResolutionProperty.addListener((_, oldResolution, newResolution) -> {
|
||||
if(newResolution != null) {
|
||||
setHeight(newResolution == WebcamResolution.HD ? (getHeight() - 100) : (getHeight() + 100));
|
||||
if(newResolution.isStandardAspect() && oldResolution.isWidescreenAspect()) {
|
||||
setWidth(getWidth());
|
||||
setHeight(getHeight() + 100);
|
||||
dialogPane.setMaxHeight(dialogPane.getPrefHeight() + 100);
|
||||
dialogPane.setPrefHeight(dialogPane.getMaxHeight());
|
||||
dialogPane.setMinHeight(dialogPane.getMaxHeight());
|
||||
} else if(newResolution.isWidescreenAspect() && oldResolution.isStandardAspect()) {
|
||||
setWidth(getWidth());
|
||||
setHeight(getHeight() - 100);
|
||||
dialogPane.setMaxHeight(dialogPane.getPrefHeight() - 100);
|
||||
dialogPane.setPrefHeight(dialogPane.getMaxHeight());
|
||||
dialogPane.setMinHeight(dialogPane.getMaxHeight());
|
||||
}
|
||||
EventManager.get().post(new WebcamResolutionChangedEvent(newResolution));
|
||||
}
|
||||
if(newResolution == null || !postOpenUpdate) {
|
||||
webcamService.cancel();
|
||||
}
|
||||
webcamService.cancel();
|
||||
});
|
||||
webcamDeviceProperty.addListener((observable, oldValue, newValue) -> {
|
||||
webcamDeviceProperty.addListener((_, _, newValue) -> {
|
||||
Config.get().setWebcamDevice(newValue.getName());
|
||||
Config.get().setWebcamDeviceId(newValue.getUniqueId());
|
||||
if(!Objects.equals(webcamService.getDevice(), newValue)) {
|
||||
webcamService.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
boolean isHdCapture = (webcamResolutionProperty.get() == WebcamResolution.HD);
|
||||
if(Config.get().isHdCapture() != isHdCapture) {
|
||||
Config.get().setHdCapture(isHdCapture);
|
||||
setOnCloseRequest(_ -> {
|
||||
if(webcamResolutionProperty.get() != null) {
|
||||
Config.get().setWebcamResolution(webcamResolutionProperty.get());
|
||||
}
|
||||
|
||||
Platform.runLater(() -> webcamResolutionProperty.set(null));
|
||||
Platform.runLater(() -> {
|
||||
webcamResolutionProperty.set(null);
|
||||
webcamService.close();
|
||||
});
|
||||
});
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType hdButtonType = new javafx.scene.control.ButtonType("Use HD Capture", ButtonBar.ButtonData.LEFT);
|
||||
final ButtonType camButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.HELP_2);
|
||||
dialogPane.getButtonTypes().addAll(hdButtonType, camButtonType, cancelButtonType);
|
||||
final ButtonType deviceButtonType = new javafx.scene.control.ButtonType("Default Camera", ButtonBar.ButtonData.LEFT);
|
||||
final ButtonType resolutionButtonType = new javafx.scene.control.ButtonType("Resolution", ButtonBar.ButtonData.HELP_2);
|
||||
dialogPane.getButtonTypes().addAll(deviceButtonType, resolutionButtonType, cancelButtonType);
|
||||
dialogPane.setPrefWidth(646);
|
||||
dialogPane.setPrefHeight(webcamResolutionProperty.get() == WebcamResolution.HD ? 490 : 590);
|
||||
dialogPane.setPrefHeight(webcamResolutionProperty.get().isWidescreenAspect() ? 490 : 590);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
@@ -548,6 +583,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
}
|
||||
|
||||
private ECKey getKey(CryptoHDKey cryptoHDKey) {
|
||||
if(cryptoHDKey.isPrivateKey()) {
|
||||
return ECKey.fromPrivate(new BigInteger(1, cryptoHDKey.getKey()));
|
||||
} else {
|
||||
return ECKey.fromPublicOnly(cryptoHDKey.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
private OutputDescriptor getOutputDescriptor(CryptoOutput cryptoOutput) {
|
||||
ScriptType scriptType = getScriptType(cryptoOutput.getScriptExpressions());
|
||||
|
||||
@@ -622,7 +665,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
|
||||
List<ChildNumber> path = cryptoKeypath.getComponents().stream().map(comp -> (IndexPathComponent)comp)
|
||||
.map(comp -> new ChildNumber(comp.getIndex(), comp.isHardened())).collect(Collectors.toList());
|
||||
String fingerprint = cryptoKeypath.getSourceFingerprint() == null ? KeystoreController.DEFAULT_WATCH_ONLY_FINGERPRINT : Utils.bytesToHex(cryptoKeypath.getSourceFingerprint());
|
||||
String fingerprint = cryptoKeypath.getSourceFingerprint() == null ? KeyDerivation.DEFAULT_WATCH_ONLY_FINGERPRINT : Utils.bytesToHex(cryptoKeypath.getSourceFingerprint());
|
||||
return new KeyDerivation(fingerprint, KeyDerivation.writePath(path));
|
||||
}
|
||||
|
||||
@@ -645,11 +688,16 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
for(int i = 0; i < keys.size(); i++) {
|
||||
RegistryItem key = keys.get(i);
|
||||
if(key instanceof URHDKey urhdKey) {
|
||||
ExtendedKey extendedKey = getExtendedKey(urhdKey);
|
||||
KeyDerivation keyDerivation = getKeyDerivation(urhdKey.getOrigin());
|
||||
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
|
||||
if(urhdKey.getName() != null) {
|
||||
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
|
||||
if(urhdKey.getChainCode() == null) {
|
||||
ECKey ecKey = getKey(urhdKey);
|
||||
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(ecKey, keyDerivation, true, true));
|
||||
} else {
|
||||
ExtendedKey extendedKey = getExtendedKey(urhdKey);
|
||||
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
|
||||
if(urhdKey.getName() != null) {
|
||||
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Only extended HD keys are supported in output descriptors");
|
||||
@@ -685,72 +733,32 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
}
|
||||
|
||||
private class QRScanListener implements WebcamListener {
|
||||
@Override
|
||||
public void webcamOpen(WebcamEvent webcamEvent) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void webcamClosed(WebcamEvent webcamEvent) {
|
||||
if(webcamResolutionProperty.get() != null) {
|
||||
webcamService.setResolution(webcamResolutionProperty.get());
|
||||
webcamService.setDevice(webcamDeviceProperty.get());
|
||||
Platform.runLater(() -> {
|
||||
if(!webcamService.isRunning()) {
|
||||
webcamService.reset();
|
||||
webcamService.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void webcamDisposed(WebcamEvent webcamEvent) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void webcamImageObtained(WebcamEvent webcamEvent) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private class QRScanDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
Node button = null;
|
||||
Node button;
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
ToggleButton hd = new ToggleButton(buttonType.getText());
|
||||
hd.setSelected(webcamResolutionProperty.get() == WebcamResolution.HD);
|
||||
hd.setGraphicTextGap(5);
|
||||
setHdGraphic(hd, hd.isSelected());
|
||||
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(hd, buttonData);
|
||||
hd.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
webcamResolutionProperty.set(newValue ? WebcamResolution.HD : WebcamResolution.VGA);
|
||||
setHdGraphic(hd, newValue);
|
||||
});
|
||||
|
||||
button = hd;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
|
||||
ComboBox<WebcamDevice> devicesCombo = new ComboBox<>(WebcamScanDriver.getFoundDevices());
|
||||
ComboBox<CaptureDevice> devicesCombo = new ComboBox<>(foundDevices);
|
||||
devicesCombo.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(WebcamDevice device) {
|
||||
return device instanceof WebcamScanDevice ? ((WebcamScanDevice)device).getDeviceName() : "Default Camera";
|
||||
public String toString(CaptureDevice device) {
|
||||
return device != null && device.getName() != null ? device.getName().replaceAll(" \\(.*\\)", "") : "Default Camera";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebcamDevice fromString(String string) {
|
||||
public CaptureDevice fromString(String string) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
});
|
||||
devicesCombo.valueProperty().bindBidirectional(webcamDeviceProperty);
|
||||
ButtonBar.setButtonData(devicesCombo, ButtonBar.ButtonData.LEFT);
|
||||
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(devicesCombo, buttonData);
|
||||
button = devicesCombo;
|
||||
} else if(buttonType.getButtonData() == ButtonBar.ButtonData.HELP_2) {
|
||||
ComboBox<WebcamResolution> resolutionsCombo = new ComboBox<>(availableResolutions);
|
||||
resolutionsCombo.valueProperty().bindBidirectional(webcamResolutionProperty);
|
||||
ButtonBar.setButtonData(resolutionsCombo, ButtonBar.ButtonData.LEFT);
|
||||
button = resolutionsCombo;
|
||||
} else {
|
||||
button = super.createButton(buttonType);
|
||||
}
|
||||
@@ -763,19 +771,39 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
button.disableProperty().bind(webcamService.openingProperty());
|
||||
return button;
|
||||
}
|
||||
}
|
||||
|
||||
private void setHdGraphic(ToggleButton hd, boolean isHd) {
|
||||
if(isHd) {
|
||||
hd.setGraphic(getGlyph(FontAwesome5.Glyph.CHECK_CIRCLE));
|
||||
public static <T extends Comparable<T>> void updateList(List<T> targetList, Collection<T> sourceList) {
|
||||
List<T> sortedSource = new ArrayList<>(sourceList);
|
||||
Collections.sort(sortedSource);
|
||||
|
||||
ListIterator<T> targetIter = targetList.listIterator();
|
||||
int sourceIndex = 0;
|
||||
|
||||
while (sourceIndex < sortedSource.size() && targetIter.hasNext()) {
|
||||
T sourceItem = sortedSource.get(sourceIndex);
|
||||
T targetItem = targetIter.next();
|
||||
int comparison = sourceItem.compareTo(targetItem);
|
||||
|
||||
if (comparison < 0) {
|
||||
targetIter.previous(); // Back up to insert before
|
||||
targetIter.add(sourceItem);
|
||||
sourceIndex++;
|
||||
} else if (comparison > 0) {
|
||||
targetIter.remove();
|
||||
} else {
|
||||
hd.setGraphic(getGlyph(FontAwesome5.Glyph.BAN));
|
||||
sourceIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
private Glyph getGlyph(FontAwesome5.Glyph glyphName) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphName);
|
||||
glyph.setFontSize(11);
|
||||
return glyph;
|
||||
while (sourceIndex < sortedSource.size()) {
|
||||
targetIter.add(sortedSource.get(sourceIndex));
|
||||
sourceIndex++;
|
||||
}
|
||||
|
||||
while (targetIter.hasNext()) {
|
||||
targetIter.next();
|
||||
targetIter.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -993,10 +1021,4 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ScanDelayCalculator implements WebcamUpdater.DelayCalculator {
|
||||
public long calculateDelay(long snapshotDuration, double deviceFps) {
|
||||
return Math.max(SCAN_PERIOD_MILLIS - snapshotDuration, 0L);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import javafx.application.Platform;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
public class RangeInputDialog extends Dialog<Integer> {
|
||||
private final Spinner<Integer> spinner;
|
||||
|
||||
public RangeInputDialog(int min, int max, int initialValue) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
|
||||
setTitle("Select a Value");
|
||||
setHeaderText("Choose a value between " + min + " and " + max);
|
||||
|
||||
Glyph key = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.SORT_NUMERIC_DOWN);
|
||||
key.setFontSize(50);
|
||||
key.setPadding(new Insets(0, 0, 0, 10));
|
||||
dialogPane.setGraphic(key);
|
||||
dialogPane.getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||
|
||||
spinner = new Spinner<>();
|
||||
spinner.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(min, max, initialValue));
|
||||
spinner.setPrefWidth(80);
|
||||
|
||||
GridPane grid = new GridPane();
|
||||
grid.setHgap(10);
|
||||
grid.setVgap(10);
|
||||
grid.setPadding(new Insets(20, 20, 10, 20));
|
||||
|
||||
grid.add(new Label("Enter value between " + min + " and " + max + ":"), 0, 0);
|
||||
grid.add(spinner, 1, 0);
|
||||
|
||||
dialogPane.setContent(grid);
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
Platform.runLater(spinner::requestFocus);
|
||||
|
||||
setResultConverter((dialogButton) -> {
|
||||
ButtonBar.ButtonData data = dialogButton == null ? null : dialogButton.getButtonData();
|
||||
return data == ButtonBar.ButtonData.OK_DONE ? spinner.getValue() : null;
|
||||
});
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(230);
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
public void setValue(int value) {
|
||||
spinner.getValueFactory().setValue(value);
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return spinner.getValue();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user