Compare commits
585 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3dc50682a3 | ||
|
|
d5b119e338 | ||
|
|
b457caa5d2 | ||
|
|
61ed816c87 | ||
|
|
9a603d7547 | ||
|
|
ac666545be | ||
|
|
c4c77d84e0 | ||
|
|
8d126869a6 | ||
|
|
464fade68f | ||
|
|
79517da131 | ||
|
|
086297436e | ||
|
|
d69e274b18 | ||
|
|
d1e67ad4a0 | ||
|
|
37ca98c2b0 | ||
|
|
287c943b44 | ||
|
|
cb92f76546 | ||
|
|
24e9c39cb8 | ||
|
|
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 | ||
|
|
0d16c87b40 | ||
|
|
b59a65dcfe | ||
|
|
87cc28e0a4 | ||
|
|
1187925543 | ||
|
|
cd4edab4ae | ||
|
|
daf320f36b | ||
|
|
f6ff92865b | ||
|
|
d420c71673 | ||
|
|
07101b3ca0 | ||
|
|
00f7f3f5b3 | ||
|
|
5d2c133401 | ||
|
|
7b0dfd66a7 | ||
|
|
83719e7df2 | ||
|
|
f1b246f0b0 | ||
|
|
599880ea5c | ||
|
|
d625bab02e | ||
|
|
1676676e06 | ||
|
|
f7e603118f | ||
|
|
f6fd889712 | ||
|
|
21d91d3d10 | ||
|
|
f1cddc28e7 | ||
|
|
1887e1c7b0 | ||
|
|
3e870f362d | ||
|
|
665d70b845 | ||
|
|
c2cbe62a5a | ||
|
|
c6b6e74515 | ||
|
|
8ddc494b53 | ||
|
|
33f439f49a | ||
|
|
d68ab40c94 | ||
|
|
31346e2afa | ||
|
|
c407a41475 | ||
|
|
8baa8e2e96 | ||
|
|
72adbe44a7 | ||
|
|
dd64c18c21 | ||
|
|
2354b061a9 | ||
|
|
a5050117a3 | ||
|
|
f245b57022 | ||
|
|
d3752a856b | ||
|
|
fe7dba6d83 | ||
|
|
2d0a94d024 | ||
|
|
41146310d6 | ||
|
|
a167f6aedb | ||
|
|
0fed7c45ee | ||
|
|
5a0df265bc | ||
|
|
646b8b0e65 | ||
|
|
c9b40b1973 | ||
|
|
9ec5b6ce26 | ||
|
|
93893111c6 | ||
|
|
3600d32ffd | ||
|
|
1e0855c11d | ||
|
|
15cb028951 | ||
|
|
e178168bec | ||
|
|
5696e00cb5 | ||
|
|
594a873f20 | ||
|
|
da30d4223a | ||
|
|
2441b4d7c3 | ||
|
|
cc739a71e9 | ||
|
|
5f98eb9eb9 | ||
|
|
5aa25b98c3 | ||
|
|
5058cd283d | ||
|
|
af6171692b | ||
|
|
3c631fa653 | ||
|
|
10a796098b | ||
|
|
8ac642b09c | ||
|
|
33d9f260c4 |
51
.github/workflows/package.yaml
vendored
51
.github/workflows/package.yaml
vendored
@ -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/
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +1,6 @@
|
||||
[submodule "drongo"]
|
||||
path = drongo
|
||||
url = ../../sparrowwallet/drongo.git
|
||||
[submodule "lark"]
|
||||
path = lark
|
||||
url = ../../sparrowwallet/lark.git
|
||||
|
||||
3
.sdkmanrc
Normal file
3
.sdkmanrc
Normal file
@ -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
README.md
16
README.md
@ -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
|
||||
|
||||
@ -64,10 +64,12 @@ Usage: sparrow [options]
|
||||
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
|
||||
--network, -n
|
||||
Network to use
|
||||
Possible Values: [mainnet, testnet, regtest, signet]
|
||||
Possible Values: [mainnet, testnet, regtest, signet, testnet4]
|
||||
```
|
||||
|
||||
As a fallback, the network (mainnet, testnet, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
Note that testnet currently refers to testnet3.
|
||||
|
||||
As a fallback, the network (mainnet, testnet, testnet4, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
|
||||
`export SPARROW_NETWORK=testnet`
|
||||
|
||||
@ -83,7 +85,7 @@ When not explicitly configured using the command line argument above, Sparrow st
|
||||
| Linux | ~/.sparrow |
|
||||
| Windows | %APPDATA%/Sparrow |
|
||||
|
||||
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
Testnet3, testnet4, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
|
||||
670
build.gradle
670
build.gradle
@ -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.8.5'
|
||||
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.3'
|
||||
|
||||
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 {
|
||||
sdk = "/home/scy/git/jfx-sandbox/build/sdk"
|
||||
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,26 +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('io.samourai.code.whirlpool:whirlpool-client:1.0.5')
|
||||
implementation('io.samourai.code.wallet:java-http-client:2.0.2')
|
||||
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.5')
|
||||
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')
|
||||
}
|
||||
|
||||
@ -151,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 {
|
||||
@ -169,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",
|
||||
@ -178,27 +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-opens=com.samourai.whirlpool.client/com.samourai.whirlpool.client.whirlpool=com.sparrowwallet.sparrow",
|
||||
"--add-opens=com.samourai.soroban.client/com.samourai.soroban.client.rpc=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=Headless", "-Dprism.order=sw"]
|
||||
applicationDefaultJvmArgs += ["-Dglass.platform=Headless"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -212,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",
|
||||
@ -229,19 +227,12 @@ 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",
|
||||
"--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-opens=com.samourai.whirlpool.client/com.samourai.whirlpool.client.whirlpool=com.sparrowwallet.sparrow",
|
||||
"--add-opens=com.samourai.soroban.client/com.samourai.soroban.client.rpc=com.sparrowwallet.sparrow",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.desktop",
|
||||
"--add-reads=com.sparrowwallet.merged.module=java.sql",
|
||||
"--add-reads=com.sparrowwallet.merged.module=com.sparrowwallet.sparrow",
|
||||
@ -253,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"]
|
||||
@ -262,55 +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=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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task removeGroupWritePermission(type: Exec) {
|
||||
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')
|
||||
}
|
||||
|
||||
tasks.register('addUserWritePermission', Exec) {
|
||||
if(os.windows) {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
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/") {
|
||||
@ -318,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')
|
||||
@ -380,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')
|
||||
@ -388,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')
|
||||
@ -451,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')
|
||||
@ -476,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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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()));
|
||||
}
|
||||
}
|
||||
@ -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.8.4"
|
||||
GIT_TAG="2.5.2"
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
|
||||
2
drongo
2
drongo
@ -1 +1 @@
|
||||
Subproject commit 9872c6b6ecfa6f5d14cd2edcb5605b76e9cb7396
|
||||
Subproject commit 077d2142cc3aad84f6f58868cf8f17fc61027fdc
|
||||
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@ -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
|
||||
|
||||
15
gradlew
vendored
15
gradlew
vendored
@ -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.
|
||||
|
||||
25
gradlew.bat
vendored
25
gradlew.bat
vendored
@ -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
|
||||
|
||||
1
lark
Submodule
1
lark
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit e9c6f35fe66aee105ef3c532fcefeb7130dab169
|
||||
48
repackage.sh
Executable file
48
repackage.sh
Executable file
@ -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
|
||||
12
src/main/deploy/package/linux-headless/control
Normal file
12
src/main/deploy/package/linux-headless/control
Normal file
@ -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}
|
||||
85
src/main/deploy/package/linux-headless/sparrowserver.spec
Executable file
85
src/main/deploy/package/linux-headless/sparrowserver.spec
Executable file
@ -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
|
||||
|
||||
12
src/main/deploy/package/linux/control
Normal file
12
src/main/deploy/package/linux/control
Normal file
@ -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}
|
||||
49
src/main/deploy/package/linux/postinst
Executable file
49
src/main/deploy/package/linux/postinst
Executable file
@ -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
src/main/deploy/package/linux/sparrowwallet.spec
Executable file
260
src/main/deploy/package/linux/sparrowwallet.spec
Executable file
@ -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.8.5</string>
|
||||
<string>2.5.3</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 |
File diff suppressed because it is too large
Load Diff
@ -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,8 +26,8 @@ import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@ -43,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;
|
||||
@ -67,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 {
|
||||
@ -88,18 +93,13 @@ 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;
|
||||
|
||||
private static AppServices INSTANCE;
|
||||
|
||||
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
|
||||
|
||||
private final SorobanServices sorobanServices = new SorobanServices();
|
||||
|
||||
private InteractionServices interactionServices;
|
||||
private final InteractionServices interactionServices;
|
||||
|
||||
private static HttpClientService httpClientService;
|
||||
|
||||
@ -109,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);
|
||||
@ -131,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;
|
||||
@ -187,9 +195,13 @@ 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);
|
||||
EventManager.get().register(whirlpoolServices);
|
||||
EventManager.get().register(sorobanServices);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@ -202,6 +214,7 @@ public class AppServices {
|
||||
preventSleepService = createPreventSleepService();
|
||||
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
minimumRelayFeeRate = getConfiguredMinimumRelayFeeRate(config);
|
||||
|
||||
if(config.getMode() == Mode.ONLINE) {
|
||||
if(config.requiresInternalTor()) {
|
||||
@ -268,7 +281,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
if(Tor.getDefault() != null) {
|
||||
Tor.getDefault().getTorManager().destroy(true, success -> {});
|
||||
Tor.getDefault().close();
|
||||
}
|
||||
}
|
||||
|
||||
@ -298,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
|
||||
@ -323,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." +
|
||||
@ -359,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;
|
||||
@ -387,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);
|
||||
|
||||
@ -487,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;
|
||||
}
|
||||
@ -502,7 +535,7 @@ public class AppServices {
|
||||
public static Proxy getProxy(String proxyCircuitId) {
|
||||
Config config = Config.get();
|
||||
Proxy proxy = null;
|
||||
if(config.isUseProxy()) {
|
||||
if(config.isUseProxy() && config.getProxyServer() != null) {
|
||||
HostAndPort proxyHostAndPort = HostAndPort.fromString(config.getProxyServer());
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
||||
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
@ -534,14 +567,6 @@ public class AppServices {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static WhirlpoolServices getWhirlpoolServices() {
|
||||
return get().whirlpoolServices;
|
||||
}
|
||||
|
||||
public static SorobanServices getSorobanServices() {
|
||||
return get().sorobanServices;
|
||||
}
|
||||
|
||||
public static InteractionServices getInteractionServices() {
|
||||
return get().interactionServices;
|
||||
}
|
||||
@ -589,6 +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"));
|
||||
@ -607,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() {
|
||||
@ -692,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);
|
||||
@ -703,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;
|
||||
}
|
||||
@ -737,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;
|
||||
}
|
||||
@ -754,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);
|
||||
}
|
||||
@ -767,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);
|
||||
@ -778,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);
|
||||
}
|
||||
@ -806,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -923,6 +1037,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
if(openWindow instanceof Stage) {
|
||||
((Stage)openWindow).setIconified(false);
|
||||
((Stage)openWindow).setAlwaysOnTop(true);
|
||||
((Stage)openWindow).setAlwaysOnTop(false);
|
||||
}
|
||||
@ -987,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 {
|
||||
@ -1007,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()) {
|
||||
@ -1081,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();
|
||||
@ -1094,17 +1208,98 @@ 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()
|
||||
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
|
||||
&& wallet.getStandardAccountType() != null
|
||||
&& StandardAccount.isMixableAccount(wallet.getStandardAccountType());
|
||||
}
|
||||
|
||||
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& 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) {
|
||||
List<Wallet> childWallets = new ArrayList<>();
|
||||
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
|
||||
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
|
||||
Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount);
|
||||
childWallets.add(childWallet);
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(storage, decryptedWallet, childWallet));
|
||||
}
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
public static Font getMonospaceFont() {
|
||||
return Font.font("Roboto Mono", 13);
|
||||
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
|
||||
@ -1119,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
|
||||
@ -1136,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
|
||||
@ -1280,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();
|
||||
}
|
||||
|
||||
76
src/main/java/com/sparrowwallet/sparrow/BlockSummary.java
Normal file
76
src/main/java/com/sparrowwallet/sparrow/BlockSummary.java
Normal file
@ -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 glassHeadless = "Headless".equalsIgnoreCase(System.getProperty("glass.platform"));
|
||||
boolean headlessPlatform = "Headless".equalsIgnoreCase(System.getProperty("glass.platform"));
|
||||
|
||||
if(headless || glassHeadless) {
|
||||
if(headless || headlessPlatform) {
|
||||
currentInterface = TERMINAL;
|
||||
|
||||
if(headless && !glassHeadless) {
|
||||
throw new UnsupportedOperationException("Headless environment detected but Headless 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.8.5";
|
||||
public static final String APP_VERSION = "2.5.3";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
public 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);
|
||||
@ -66,6 +76,11 @@ public class SparrowWallet {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
File testnet4Flag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET4.getName());
|
||||
if(testnet4Flag.exists()) {
|
||||
Network.set(Network.TESTNET4);
|
||||
}
|
||||
|
||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||
if(signetFlag.exists()) {
|
||||
Network.set(Network.SIGNET);
|
||||
|
||||
@ -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,21 +1,18 @@
|
||||
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;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
|
||||
|
||||
@ -46,12 +43,14 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
standardAccountCombo = new ComboBox<>();
|
||||
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
List<Integer> existingIndexes = new ArrayList<>();
|
||||
Set<Integer> existingIndexes = new LinkedHashSet<>();
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
existingIndexes.add(masterWallet.getAccountIndex());
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
existingIndexes.add(childWallet.getAccountIndex());
|
||||
Optional<StandardAccount> optStdAcc = Arrays.stream(StandardAccount.values()).filter(stdacc -> stdacc.getName().equals(childWallet.getName())).findFirst();
|
||||
optStdAcc.ifPresent(standardAccount -> existingIndexes.add(standardAccount.getAccountNumber()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,16 +61,15 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
}
|
||||
}
|
||||
|
||||
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
if(AppServices.isWhirlpoolCompatible(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
availableAccounts.add(WHIRLPOOL_PREMIX);
|
||||
} else if(WhirlpoolServices.canWatchPostmix(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
|
||||
} else if(AppServices.isWhirlpoolPostmixCompatible(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
|
||||
availableAccounts.add(WHIRLPOOL_POSTMIX);
|
||||
}
|
||||
|
||||
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
||||
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
|
||||
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||
if(!availableAccounts.isEmpty() && (masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
|| (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);
|
||||
}
|
||||
}
|
||||
372
src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java
Normal file
372
src/main/java/com/sparrowwallet/sparrow/control/BlockCube.java
Normal file
@ -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);
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import javafx.beans.value.ChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
@ -52,6 +53,7 @@ public class CopyableTextField extends CustomTextField {
|
||||
selectedTextProperty().removeListener(selectionListener);
|
||||
}
|
||||
});
|
||||
setContextMenu(new ContextMenu());
|
||||
}
|
||||
|
||||
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
|
||||
|
||||
@ -11,7 +11,7 @@ import java.util.Date;
|
||||
public class DateAxisFormatter extends StringConverter<Number> {
|
||||
private static final DateFormat HOUR_FORMAT = new SimpleDateFormat("HH:mm");
|
||||
private static final DateFormat DAY_FORMAT = new SimpleDateFormat("d MMM");
|
||||
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yy");
|
||||
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yyyy");
|
||||
|
||||
private final DateFormat dateFormat;
|
||||
private int oddCounter;
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -3,13 +3,14 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.PdfUtils;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Button;
|
||||
|
||||
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
|
||||
super(ur);
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, 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);
|
||||
@ -19,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);
|
||||
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 -> {
|
||||
@ -606,7 +620,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionContextMenu extends ContextMenu {
|
||||
protected static class TransactionContextMenu extends ContextMenu {
|
||||
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
@ -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,14 +8,19 @@ 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;
|
||||
|
||||
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);
|
||||
@ -25,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
|
||||
@ -45,30 +51,94 @@ public class FeeRangeSlider extends Slider {
|
||||
updateMaxFeeRange(newValue.doubleValue());
|
||||
}
|
||||
});
|
||||
|
||||
setOnScroll(event -> {
|
||||
if(event.getDeltaY() != 0) {
|
||||
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
|
||||
if(newFeeRate < AppServices.getLongFeeRatesRange().getFirst()) {
|
||||
newFeeRate = AppServices.getLongFeeRatesRange().getFirst();
|
||||
} else if(newFeeRate > AppServices.getLongFeeRatesRange().getLast()) {
|
||||
newFeeRate = AppServices.getLongFeeRatesRange().getLast();
|
||||
}
|
||||
setFeeRate(newFeeRate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
@ -123,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")
|
||||
);
|
||||
@ -240,6 +240,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
javafx.application.Platform.runLater(passwordField::requestFocus);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,11 +25,11 @@ 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())) {
|
||||
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + keystore.getKeyDerivation().getDerivationPath() + ".");
|
||||
setError("Incorrect derivation", "This account requires a derivation of " + requiredDerivation.getDerivationPath() + ", but the imported keystore has a derivation of " + KeyDerivation.writePath(keystore.getKeyDerivation().getDerivation()) + ".");
|
||||
} else {
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
|
||||
@ -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,14 +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, true, 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());
|
||||
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);
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR());
|
||||
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), 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, 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;
|
||||
}
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@ -29,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);
|
||||
@ -38,28 +39,38 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private final KeystoreFileImport importer;
|
||||
private String fileName;
|
||||
private byte[] fileBytes;
|
||||
private String password;
|
||||
|
||||
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), importer.getWalletModel(), importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||
this.fileName = fileName;
|
||||
this.password = password;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||
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 {
|
||||
@ -69,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, "");
|
||||
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);
|
||||
@ -130,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();
|
||||
@ -151,6 +165,14 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
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) {
|
||||
@ -154,6 +160,17 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
signature.setStyle("-fx-pref-height: 80px");
|
||||
signature.setWrapText(true);
|
||||
signature.setOnMouseClicked(event -> signature.selectAll());
|
||||
|
||||
ContextMenu signatureMenu = new ContextMenu();
|
||||
MenuItem copyItem = new MenuItem("Copy");
|
||||
copyItem.setOnAction(e -> signature.copy());
|
||||
MenuItem pasteItem = new MenuItem("Paste");
|
||||
pasteItem.setOnAction(e -> signature.paste());
|
||||
MenuItem clearItem = new MenuItem("Clear");
|
||||
clearItem.setOnAction(e -> signature.clear());
|
||||
signatureMenu.getItems().addAll(copyItem, pasteItem, clearItem);
|
||||
signature.setContextMenu(signatureMenu);
|
||||
|
||||
signatureField.getInputs().add(signature);
|
||||
|
||||
Field formatField = new Field();
|
||||
@ -199,13 +216,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 +255,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 +288,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 +298,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 +306,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 +332,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 +374,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 +387,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 +413,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 +481,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 +495,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 +513,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 +606,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 +776,37 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
decryptWalletService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageSignDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
SplitMenuButton signByButton = new SplitMenuButton();
|
||||
signByButton.setText("Sign by QR");
|
||||
signByButton.setDisable(wallet == null);
|
||||
signByButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
|
||||
signByButton.setGraphicTextGap(5);
|
||||
signByButton.setOnAction(event -> {
|
||||
showQr();
|
||||
});
|
||||
MenuItem exportFile = new MenuItem("Sign by File...");
|
||||
exportFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_EXPORT)));
|
||||
exportFile.setOnAction(event -> {
|
||||
exportFile();
|
||||
});
|
||||
MenuItem importFile = new MenuItem("Load Signed File...");
|
||||
importFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_IMPORT)));
|
||||
importFile.setOnAction(event -> {
|
||||
importFile();
|
||||
});
|
||||
signByButton.getItems().addAll(exportFile, importFile);
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(signByButton, buttonData);
|
||||
|
||||
return signByButton;
|
||||
}
|
||||
|
||||
return super.createButton(buttonType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,27 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.mix.listener.MixStep;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
|
||||
|
||||
public MixStatusCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
@ -41,162 +25,9 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(Integer.toString(mixStatus.getMixesDone()));
|
||||
if(mixStatus.getNextMixUtxo() == null) {
|
||||
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
}
|
||||
|
||||
if(mixStatus.getNextMixUtxo() != null) {
|
||||
setMixSuccess(mixStatus.getNextMixUtxo());
|
||||
} else if(mixStatus.getMixFailReason() != null) {
|
||||
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
|
||||
} else if(mixStatus.getMixProgress() != null) {
|
||||
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setMixSuccess(Utxo nextMixUtxo) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(-1);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
|
||||
setTooltip(tt);
|
||||
}
|
||||
|
||||
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
|
||||
if(mixFailReason.isError()) {
|
||||
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
|
||||
if(elapsed >= ERROR_DISPLAY_MILLIS) {
|
||||
//Old error, don't set again.
|
||||
return;
|
||||
}
|
||||
|
||||
Glyph failGlyph = getFailGlyph();
|
||||
setGraphic(failGlyph);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
|
||||
"\nMix failures are generally caused by peers disconnecting during a mix." +
|
||||
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
|
||||
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
|
||||
setTooltip(tt);
|
||||
|
||||
Duration fadeDuration = Duration.millis(ERROR_DISPLAY_MILLIS - elapsed);
|
||||
double fadeFromValue = 1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS);
|
||||
Timeline timeline = AnimationUtil.getSlowFadeOut(failGlyph, fadeDuration, fadeFromValue, 10);
|
||||
timeline.setOnFinished(event -> {
|
||||
setTooltip(null);
|
||||
});
|
||||
timeline.play();
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPlatformSleepConfig() {
|
||||
Platform platform = Platform.getCurrent();
|
||||
if(platform == Platform.OSX) {
|
||||
return "OSX System Preferences";
|
||||
} else if(platform == Platform.WINDOWS) {
|
||||
return "Windows Control Panel";
|
||||
}
|
||||
|
||||
return "system power settings";
|
||||
}
|
||||
|
||||
private void setMixProgress(UtxoEntry utxoEntry, MixProgress mixProgress) {
|
||||
if(mixProgress.getMixStep() != MixStep.FAIL) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
String status = mixProgress.getMixStep().getMessage().replaceAll("_", " ");
|
||||
status = status.substring(0, 1).toUpperCase(Locale.ROOT) + status.substring(1).toLowerCase(Locale.ROOT);
|
||||
tt.setText(status);
|
||||
setTooltip(tt);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private ProgressIndicator getProgressIndicator() {
|
||||
ProgressIndicator progressIndicator;
|
||||
if(getGraphic() instanceof ProgressIndicator) {
|
||||
progressIndicator = (ProgressIndicator)getGraphic();
|
||||
} else {
|
||||
progressIndicator = new ProgressBar();
|
||||
}
|
||||
|
||||
return progressIndicator;
|
||||
}
|
||||
|
||||
private static Glyph getMixGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getStopGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFailGlyph() {
|
||||
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
failGlyph.getStyleClass().add("fail-warning");
|
||||
failGlyph.setFontSize(12);
|
||||
return failGlyph;
|
||||
}
|
||||
|
||||
private static class MixStatusContextMenu extends ContextMenu {
|
||||
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
|
||||
Whirlpool pool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(isMixing) {
|
||||
MenuItem mixStop = new MenuItem("Stop Mixing");
|
||||
if(pool != null) {
|
||||
mixStop.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
mixStop.setGraphic(getStopGlyph());
|
||||
mixStop.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mixStop(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixStop);
|
||||
} else {
|
||||
MenuItem mixNow = new MenuItem("Mix Now");
|
||||
if(pool != null) {
|
||||
mixNow.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
|
||||
mixNow.setGraphic(getMixGlyph());
|
||||
mixNow.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mix(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ import java.util.Optional;
|
||||
public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected final Wallet wallet;
|
||||
private final KeystoreMnemonicImport importer;
|
||||
private final KeyDerivation defaultDerivation;
|
||||
|
||||
private SplitMenuButton importButton;
|
||||
|
||||
@ -43,10 +44,11 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
private Button confirmButton;
|
||||
private List<String> generatedMnemonicCode;
|
||||
|
||||
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer) {
|
||||
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
public MnemonicKeystoreImportPane(Wallet wallet, KeystoreMnemonicImport importer, KeyDerivation defaultDerivation) {
|
||||
super(importer.getName(), "Create or enter seed", importer.getKeystoreImportDescription(), importer.getWalletModel());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
|
||||
createImportButton();
|
||||
buttonBox.getChildren().add(importButton);
|
||||
@ -56,10 +58,10 @@ 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(wallet.getScriptType().getDefaultDerivation(), false);
|
||||
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;
|
||||
@ -77,6 +79,10 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
importButton.setVisible(false);
|
||||
}
|
||||
|
||||
private List<ChildNumber> getDefaultDerivation() {
|
||||
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
protected void enterMnemonic(int numWords) {
|
||||
generatedMnemonicCode = null;
|
||||
super.enterMnemonic(numWords);
|
||||
@ -135,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) {
|
||||
@ -243,14 +249,14 @@ public class MnemonicKeystoreImportPane extends MnemonicKeystorePane {
|
||||
setDescription("Ready to import");
|
||||
showHideLink.setText("Show Derivation...");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getDerivationEntry(wallet.getScriptType().getDefaultDerivation()));
|
||||
setContent(getDerivationEntry(getDefaultDerivation()));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -79,10 +79,6 @@ public class PayNymAvatar extends StackPane {
|
||||
this.paymentCodeProperty.set(paymentCode);
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
setPaymentCode(PaymentCode.fromString(paymentCode.toString()));
|
||||
}
|
||||
|
||||
public void clearPaymentCode() {
|
||||
this.paymentCodeProperty.set(null);
|
||||
}
|
||||
@ -128,8 +124,11 @@ public class PayNymAvatar extends StackPane {
|
||||
log.debug("Requesting PayNym avatar from " + url);
|
||||
}
|
||||
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
|
||||
Image image = new Image(is, 150, 150, true, false);
|
||||
try(InputStream is = (proxy == null ? new URI(url).toURL().openStream() : new URI(url).toURL().openConnection(proxy).getInputStream())) {
|
||||
Image image = new Image(is, 150, 150, true, true);
|
||||
if(image.getException() != null) {
|
||||
throw image.getException();
|
||||
}
|
||||
paymentCodeCache.put(cacheId, image);
|
||||
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
|
||||
return image;
|
||||
|
||||
@ -81,10 +81,7 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||
linkButton.setDisable(true);
|
||||
payNymController.linkPayNym(payNym);
|
||||
});
|
||||
|
||||
if(payNymController.isSelectLinkedOnly()) {
|
||||
getStyleClass().add("unlinked");
|
||||
}
|
||||
getStyleClass().add("unlinked");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,11 +10,6 @@ public class PaymentCodeTextField extends CopyableTextField {
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
this.paymentCodeStr = paymentCode.toString();
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
private void setPaymentCodeString() {
|
||||
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
|
||||
setText(abbrevPcode);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user