mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 12:06:15 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0dce4783ef | ||
|
|
cf797ea0f4 | ||
|
|
969dd72d6d | ||
|
|
2d667d118a | ||
|
|
1b8e9b5229 | ||
|
|
aeb0eeb11a | ||
|
|
febece9e9b | ||
|
|
432758d22a | ||
|
|
8b410ad005 | ||
|
|
abc0a4f2b7 | ||
|
|
6b30e6a3ec | ||
|
|
a5f705f185 | ||
|
|
d7c726782c | ||
|
|
b3d51bce34 | ||
|
|
d8e7b54cae | ||
|
|
078af174af | ||
|
|
4b8a4594b0 | ||
|
|
9a870ba522 | ||
|
|
b0591d7afe | ||
|
|
e3db43679f | ||
|
|
fb10ac3aa2 | ||
|
|
08b36abb6a | ||
|
|
fa9194a032 | ||
|
|
73897fc53a | ||
|
|
72c1d822a4 | ||
|
|
a13016609d | ||
|
|
c7798d920e | ||
|
|
5ae36a549f | ||
|
|
f7f6b59d6d | ||
|
|
771f2dc9bc | ||
|
|
a9e144bb0c | ||
|
|
0b8199948d | ||
|
|
f697aa610b | ||
|
|
37bab9f321 | ||
|
|
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 | ||
|
|
86247c6440 | ||
|
|
c39425ed3b | ||
|
|
e5e94c3ea6 | ||
|
|
9ba4458f48 | ||
|
|
a9fd7c263f | ||
|
|
6ef1313137 | ||
|
|
8e66db0237 | ||
|
|
6da98befe1 | ||
|
|
6b4c301458 | ||
|
|
86ff7b8cf9 | ||
|
|
a805d9e036 | ||
|
|
f0bfc44e72 | ||
|
|
0fad93524e | ||
|
|
c1fc8712d5 | ||
|
|
5d674b7e91 | ||
|
|
d1a353ae53 | ||
|
|
08ec158d19 | ||
|
|
2e8112cba0 | ||
|
|
c108741b6f | ||
|
|
4450d625dd | ||
|
|
2e1ee0c5b2 | ||
|
|
1c3c2d8089 | ||
|
|
a4991064af | ||
|
|
6ea6f4b5d2 | ||
|
|
210d52c001 | ||
|
|
fcbed8795f | ||
|
|
0d9e798bb7 | ||
|
|
9d0c35bc75 | ||
|
|
f3c44e6f3e | ||
|
|
14d0437424 | ||
|
|
d2934c94c5 | ||
|
|
2e847199f5 | ||
|
|
c9d1650ed4 | ||
|
|
da74089969 | ||
|
|
d1ac5b076e | ||
|
|
e1564217ed | ||
|
|
2ef66d504f | ||
|
|
f0bd07b4b7 | ||
|
|
e43b783664 | ||
|
|
2d5e24366c | ||
|
|
195854fb6f | ||
|
|
9e4eed965c | ||
|
|
c034dbe89e | ||
|
|
4cb2e1ef9f | ||
|
|
7258b049c9 | ||
|
|
5475a81e3a | ||
|
|
f003b6d732 | ||
|
|
249a01c208 | ||
|
|
c34a423f95 | ||
|
|
55e7689f7c | ||
|
|
914afe9a8d | ||
|
|
81c7bc7ecb | ||
|
|
3d18477560 | ||
|
|
2c1f591c51 | ||
|
|
d109eaa654 | ||
|
|
803e43cb45 | ||
|
|
a45024ac70 | ||
|
|
0f05502af6 | ||
|
|
6a496894e1 | ||
|
|
d3b0eac51a | ||
|
|
2cc02e38e6 | ||
|
|
ae29108656 | ||
|
|
0ed8c6af7c | ||
|
|
6d7f02227a | ||
|
|
8f52039c7b | ||
|
|
f14e2fb020 | ||
|
|
cc9a557a2e | ||
|
|
e50cc7126d | ||
|
|
6cdbba4bb3 | ||
|
|
7f3885611a | ||
|
|
17ea75603f | ||
|
|
da1626070b | ||
|
|
6f4d37d3ff | ||
|
|
78b0c63f87 | ||
|
|
14d96bdb4e | ||
|
|
d73820464e | ||
|
|
ff4ff90bb2 | ||
|
|
d2940265fd | ||
|
|
3f72a84afe | ||
|
|
521bbdd70e | ||
|
|
15c3150a4f | ||
|
|
865c52e5d9 | ||
|
|
ee195f2677 | ||
|
|
1d50b4f296 | ||
|
|
22310cd8c9 | ||
|
|
78406fd024 | ||
|
|
74a551ed01 | ||
|
|
4de6bd5e83 | ||
|
|
8d6230e834 | ||
|
|
31042039d7 | ||
|
|
1ba501f5c8 | ||
|
|
55d5a97d99 | ||
|
|
37af663511 | ||
|
|
b757c76028 | ||
|
|
82e0b63ed0 | ||
|
|
7da3a55489 | ||
|
|
7ebb92d90a | ||
|
|
1d32b69345 | ||
|
|
fbc49fd6f5 | ||
|
|
30001051c7 | ||
|
|
1e74ae5f19 | ||
|
|
6fc52fdc0e | ||
|
|
1d2081d2a6 | ||
|
|
4d587cf776 | ||
|
|
14689dd256 | ||
|
|
c8a5c1fb86 | ||
|
|
734818cd9e | ||
|
|
6aa5473b24 | ||
|
|
1b460533f5 | ||
|
|
ad1ecfb887 | ||
|
|
6a5060c0c8 | ||
|
|
02a0a3277b | ||
|
|
57dba5d6ae | ||
|
|
540424a2e3 | ||
|
|
e1d9d54da3 | ||
|
|
2b8a513adf | ||
|
|
20df1dbd8b | ||
|
|
04c8017bb5 | ||
|
|
be0ac52a09 | ||
|
|
3162371838 | ||
|
|
b5196d1ac2 | ||
|
|
343cb2271c | ||
|
|
4feb4a3a79 | ||
|
|
20b4d5a1b5 | ||
|
|
06489d8339 | ||
|
|
e368782b4c | ||
|
|
6ee3755ce4 | ||
|
|
d425933189 | ||
|
|
675b7ed4f8 | ||
|
|
6072f6d31a | ||
|
|
e1fb674170 | ||
|
|
d8839763a4 | ||
|
|
64e98cdb35 | ||
|
|
bde8fef35e | ||
|
|
87e7a87e5e | ||
|
|
85eb4df7e9 | ||
|
|
bb32a1e7b1 | ||
|
|
f590794589 | ||
|
|
1e3ce7eb88 | ||
|
|
ef3e2ed695 | ||
|
|
74c3370277 | ||
|
|
170e7c0abf | ||
|
|
e0486ff4a9 | ||
|
|
1d17384152 | ||
|
|
aec26d512b | ||
|
|
9e5a6e83d1 | ||
|
|
eecb90e9b2 | ||
|
|
b3516063b2 | ||
|
|
1d560d6aa6 | ||
|
|
d128401a09 | ||
|
|
9391a397da | ||
|
|
a5312374a8 | ||
|
|
c81c42a87c | ||
|
|
d84ade5b7d | ||
|
|
323d29e34a |
@@ -0,0 +1,110 @@
|
||||
name: Update Gradle verification metadata
|
||||
description: Restore optional previous Gradle verification files, update them, validate them, and upload the result.
|
||||
|
||||
inputs:
|
||||
output-artifact:
|
||||
description: Name of the artifact to upload with the updated verification files.
|
||||
required: true
|
||||
previous-artifact:
|
||||
description: Optional previous verification artifact to restore before running Gradle.
|
||||
required: false
|
||||
default: ''
|
||||
clean-start:
|
||||
description: Remove existing verification files before bootstrapping metadata.
|
||||
required: false
|
||||
default: 'false'
|
||||
cache-gradle-wrapper-distribution:
|
||||
description: Cache only the Gradle wrapper distribution, not Gradle dependency artifacts.
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Download previous verification files
|
||||
if: ${{ inputs.previous-artifact != '' }}
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ${{ inputs.previous-artifact }}
|
||||
path: build/previous-verification
|
||||
|
||||
- name: Restore previous verification files
|
||||
if: ${{ inputs.previous-artifact != '' }}
|
||||
shell: bash
|
||||
run: cp build/previous-verification/gradle/verification-* gradle/
|
||||
|
||||
- name: Bootstrap verification metadata
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p gradle
|
||||
if [ "${{ inputs.clean-start }}" = "true" ]; then
|
||||
rm -f gradle/verification-metadata.xml gradle/verification-keyring.keys
|
||||
fi
|
||||
if [ ! -f gradle/verification-metadata.xml ]; then
|
||||
{
|
||||
printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>'
|
||||
printf '%s\n' '<verification-metadata xmlns="https://schema.gradle.org/dependency-verification" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd">'
|
||||
printf '%s\n' ' <configuration>'
|
||||
printf '%s\n' ' <verify-metadata>true</verify-metadata>'
|
||||
printf '%s\n' ' <verify-signatures>true</verify-signatures>'
|
||||
printf '%s\n' ' <keyring-format>armored</keyring-format>'
|
||||
printf '%s\n' ' <key-servers>'
|
||||
printf '%s\n' ' <key-server uri="https://keyserver.ubuntu.com"/>'
|
||||
printf '%s\n' ' <key-server uri="https://keys.openpgp.org"/>'
|
||||
printf '%s\n' ' </key-servers>'
|
||||
printf '%s\n' ' <trusted-artifacts>'
|
||||
printf '%s\n' ' <trust file=".*-javadoc[.]jar" regex="true"/>'
|
||||
printf '%s\n' ' <trust file=".*-sources[.]jar" regex="true"/>'
|
||||
printf '%s\n' ' </trusted-artifacts>'
|
||||
printf '%s\n' ' </configuration>'
|
||||
printf '%s\n' ' <components>'
|
||||
printf '%s\n' ' </components>'
|
||||
printf '%s\n' '</verification-metadata>'
|
||||
} > gradle/verification-metadata.xml
|
||||
fi
|
||||
|
||||
- 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@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '25.0.2'
|
||||
|
||||
- name: Cache Gradle wrapper distribution
|
||||
if: ${{ inputs.cache-gradle-wrapper-distribution == 'true' }}
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.gradle/wrapper/dists
|
||||
key: gradle-wrapper-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('gradle/wrapper/gradle-wrapper.properties') }}
|
||||
|
||||
- name: Generate dependency verification metadata
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew --write-verification-metadata sha512 --refresh-keys \
|
||||
dependencies :drongo:dependencies :lark:dependencies jpackageImage
|
||||
./gradlew --write-verification-metadata pgp,sha512 --refresh-keys \
|
||||
dependencies :drongo:dependencies :lark:dependencies jpackageImage
|
||||
./gradlew --export-keys help
|
||||
|
||||
- name: Validate dependency verification metadata
|
||||
shell: bash
|
||||
run: |
|
||||
./gradlew help dependencies :drongo:dependencies :lark:dependencies \
|
||||
--dependency-verification strict --refresh-keys
|
||||
|
||||
- name: Stage verification files
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p build/dependency-verification/gradle
|
||||
cp gradle/verification-metadata.xml build/dependency-verification/gradle/
|
||||
cp gradle/verification-keyring.keys build/dependency-verification/gradle/
|
||||
|
||||
- name: Upload verification files
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ inputs.output-artifact }}
|
||||
path: build/dependency-verification
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,187 @@
|
||||
name: Dependency Verification
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '17 3 * * *'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
ALLOW_ADDITIONAL_ARTIFACT_CHECKSUMS: 'false'
|
||||
CLEAN_STALE_VERIFICATION_METADATA: 'false'
|
||||
CACHE_GRADLE_WRAPPER_DISTRIBUTION: 'true'
|
||||
JDK_JAVAC_OPTIONS: '-Xlint:-module'
|
||||
|
||||
concurrency:
|
||||
group: dependency-verification-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
linux-x64:
|
||||
name: Generate (Linux x64)
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/update-gradle-verification
|
||||
with:
|
||||
cache-gradle-wrapper-distribution: ${{ env.CACHE_GRADLE_WRAPPER_DISTRIBUTION }}
|
||||
clean-start: ${{ env.CLEAN_STALE_VERIFICATION_METADATA }}
|
||||
output-artifact: gradle-verification-linux-x64
|
||||
|
||||
linux-arm64:
|
||||
name: Generate (Linux arm64)
|
||||
runs-on: ubuntu-22.04-arm
|
||||
needs: linux-x64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/update-gradle-verification
|
||||
with:
|
||||
cache-gradle-wrapper-distribution: ${{ env.CACHE_GRADLE_WRAPPER_DISTRIBUTION }}
|
||||
previous-artifact: gradle-verification-linux-x64
|
||||
output-artifact: gradle-verification-linux-arm64
|
||||
|
||||
windows-x64:
|
||||
name: Generate (Windows x64)
|
||||
runs-on: windows-2022
|
||||
needs: linux-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/update-gradle-verification
|
||||
with:
|
||||
cache-gradle-wrapper-distribution: ${{ env.CACHE_GRADLE_WRAPPER_DISTRIBUTION }}
|
||||
previous-artifact: gradle-verification-linux-arm64
|
||||
output-artifact: gradle-verification-windows-x64
|
||||
|
||||
macos-x64:
|
||||
name: Generate (macOS x64)
|
||||
runs-on: macos-15-intel
|
||||
needs: windows-x64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/update-gradle-verification
|
||||
with:
|
||||
cache-gradle-wrapper-distribution: ${{ env.CACHE_GRADLE_WRAPPER_DISTRIBUTION }}
|
||||
previous-artifact: gradle-verification-windows-x64
|
||||
output-artifact: gradle-verification-macos-x64
|
||||
|
||||
macos-arm64:
|
||||
name: Generate (macOS arm64)
|
||||
runs-on: macos-14
|
||||
needs: macos-x64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: ./.github/actions/update-gradle-verification
|
||||
with:
|
||||
cache-gradle-wrapper-distribution: ${{ env.CACHE_GRADLE_WRAPPER_DISTRIBUTION }}
|
||||
previous-artifact: gradle-verification-macos-x64
|
||||
output-artifact: gradle-verification-final
|
||||
|
||||
update-pr:
|
||||
name: Open update PR
|
||||
runs-on: ubuntu-22.04
|
||||
needs: macos-arm64
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Download final verification files
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: gradle-verification-final
|
||||
path: build/final-verification
|
||||
|
||||
- name: Create or update pull request
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
BRANCH_NAME: automation/update-gradle-verification-metadata
|
||||
run: |
|
||||
verification_metadata_diff() {
|
||||
if git ls-files --error-unmatch gradle/verification-metadata.xml >/dev/null 2>&1; then
|
||||
git diff -- gradle/verification-metadata.xml
|
||||
else
|
||||
git diff --no-index -- /dev/null gradle/verification-metadata.xml || true
|
||||
fi
|
||||
}
|
||||
|
||||
reset_verification_files() {
|
||||
for file in gradle/verification-metadata.xml gradle/verification-keyring.keys; do
|
||||
if git ls-files --error-unmatch "$file" >/dev/null 2>&1; then
|
||||
git restore "$file"
|
||||
else
|
||||
rm -f "$file"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
mkdir -p /tmp/dependency-verification/gradle
|
||||
cp build/final-verification/gradle/verification-* /tmp/dependency-verification/gradle/
|
||||
cp /tmp/dependency-verification/gradle/verification-* gradle/
|
||||
|
||||
if [ -z "$(git status --porcelain -- gradle/verification-metadata.xml gradle/verification-keyring.keys)" ]; then
|
||||
echo "No dependency verification changes detected."
|
||||
exit 0
|
||||
fi
|
||||
if [ "$ALLOW_ADDITIONAL_ARTIFACT_CHECKSUMS" != "true" ] && verification_metadata_diff | grep -q '^[+] *<also-trust '; then
|
||||
echo "Generated verification metadata adds also-trust checksums."
|
||||
echo "This indicates an existing artifact coordinate resolved to a checksum not currently trusted by the repository."
|
||||
echo "Set ALLOW_ADDITIONAL_ARTIFACT_CHECKSUMS to 'true' to allow opening an automated metadata update PR for this case."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
if git fetch origin "+refs/heads/$BRANCH_NAME:refs/remotes/origin/$BRANCH_NAME"; then
|
||||
mkdir -p /tmp/dependency-verification/existing/gradle
|
||||
if git show "refs/remotes/origin/$BRANCH_NAME:gradle/verification-metadata.xml" > /tmp/dependency-verification/existing/gradle/verification-metadata.xml &&
|
||||
git show "refs/remotes/origin/$BRANCH_NAME:gradle/verification-keyring.keys" > /tmp/dependency-verification/existing/gradle/verification-keyring.keys &&
|
||||
cmp -s /tmp/dependency-verification/gradle/verification-metadata.xml /tmp/dependency-verification/existing/gradle/verification-metadata.xml &&
|
||||
cmp -s /tmp/dependency-verification/gradle/verification-keyring.keys /tmp/dependency-verification/existing/gradle/verification-keyring.keys &&
|
||||
git merge-base --is-ancestor HEAD "refs/remotes/origin/$BRANCH_NAME"; then
|
||||
echo "Existing automation branch already contains these verification changes."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
reset_verification_files
|
||||
git checkout -B "$BRANCH_NAME"
|
||||
cp /tmp/dependency-verification/gradle/verification-* gradle/
|
||||
git add gradle/verification-metadata.xml gradle/verification-keyring.keys
|
||||
git commit -m "Update Gradle dependency verification metadata"
|
||||
git push --force-with-lease origin "HEAD:$BRANCH_NAME"
|
||||
|
||||
if gh pr list --base master --head "$BRANCH_NAME" --json number --jq '.[0].number' | grep -q .; then
|
||||
echo "Existing dependency verification PR found; branch was updated."
|
||||
else
|
||||
gh pr create \
|
||||
--base master \
|
||||
--head "$BRANCH_NAME" \
|
||||
--title "Update Gradle dependency verification metadata" \
|
||||
--body "Regenerates Gradle dependency verification metadata across Linux, Windows, and macOS using Gradle's incremental verification metadata update flow."
|
||||
fi
|
||||
@@ -5,52 +5,72 @@ on: workflow_dispatch
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
JDK_JAVAC_OPTIONS: '-Xlint:-module'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
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@v3
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 18.0.1
|
||||
uses: actions/setup-java@v3
|
||||
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@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
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@6eeb7bf9b882cf89ff96b77f99bcf487b9d33992 # 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@v3
|
||||
- name: Repackage Linux deb distribution
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
run: ./repackage.sh
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
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@v3
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Sparrow Build - ${{ runner.os }} Headless
|
||||
name: Sparrow Build - ${{ runner.os }} ${{ runner.arch }} Headless
|
||||
path: |
|
||||
build/jpackage/*
|
||||
!build/jpackage/Sparrow/
|
||||
@@ -1,3 +1,6 @@
|
||||
[submodule "drongo"]
|
||||
path = drongo
|
||||
url = ../../sparrowwallet/drongo.git
|
||||
[submodule "lark"]
|
||||
path = lark
|
||||
url = ../../sparrowwallet/lark.git
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Enable auto-env through the sdkman_auto_env config
|
||||
# Add key=value pairs of SDKs to use below
|
||||
java=25.0.2-tem
|
||||
@@ -16,14 +16,14 @@ or for those without SSH credentials:
|
||||
|
||||
`git clone --recursive https://github.com/sparrowwallet/sparrow.git`
|
||||
|
||||
In order to build, Sparrow requires Java 18 or higher to be installed.
|
||||
The release binaries are built with [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
|
||||
In order to build, Sparrow requires Java 25 or higher to be installed.
|
||||
The release binaries are built with [Eclipse Temurin 25.0.2+10](https://github.com/adoptium/temurin25-binaries/releases/tag/jdk-25.0.2%2B10).
|
||||
If you are using [SDKMAN](https://sdkman.io/), you can use `sdk env install` to ensure you have the correct version.
|
||||
|
||||
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
|
||||
|
||||
`sudo apt install -y rpm fakeroot binutils`
|
||||
|
||||
|
||||
The Sparrow binaries can be built from source using
|
||||
|
||||
`./gradlew jpackage`
|
||||
@@ -44,7 +44,7 @@ If you prefer to run Sparrow directly from source, it can be launched from withi
|
||||
|
||||
`./sparrow`
|
||||
|
||||
Java 18 or higher must be installed.
|
||||
Java 25 or higher must be installed.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -79,11 +81,23 @@ When not explicitly configured using the command line argument above, Sparrow st
|
||||
|
||||
| Platform | Location |
|
||||
|----------| -------- |
|
||||
| OSX | ~/.sparrow |
|
||||
| macOS | ~/.sparrow |
|
||||
| Linux | ~/.sparrow |
|
||||
| Windows | %APPDATA%/Sparrow |
|
||||
|
||||
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
Testnet3, testnet4, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
|
||||
On macOS and Linux, Sparrow also supports the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/latest/).
|
||||
This is opt in: for each category below, if the corresponding directory already exists, Sparrow uses it, otherwise it continues to use the home folder above. Categories are resolved independently, so files can be moved across one at a time.
|
||||
|
||||
| Category | Location | Contents |
|
||||
|----------| -------- |---------------------------------------|
|
||||
| Config | `$XDG_CONFIG_HOME/sparrow` (default `~/.config/sparrow`) | `config`, `network-*` markers |
|
||||
| Data | `$XDG_DATA_HOME/sparrow` (default `~/.local/share/sparrow`) | `wallets`, `certs`, `lark` |
|
||||
| State | `$XDG_STATE_HOME/sparrow` (default `~/.local/state/sparrow`) | `sparrow.log`, `tor/work`, lock files |
|
||||
| Cache | `$XDG_CACHE_HOME/sparrow` (default `~/.cache/sparrow`) | `tor/cache` |
|
||||
|
||||
Specifying a home folder with the `-d` argument disables XDG resolution entirely, and stores all files in the given folder.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
|
||||
+316
-481
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
||||
plugins {
|
||||
id 'java-gradle-plugin' // so we can assign and ID to our plugin
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.ow2.asm:asm:8.0.1'
|
||||
implementation 'com.google.gradle:osdetector-gradle-plugin:1.7.0'
|
||||
implementation 'org.javamodularity:moduleplugin:1.8.12'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
}
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
// here we register our plugin with an ID
|
||||
register("extra-java-module-info") {
|
||||
id = "extra-java-module-info"
|
||||
implementationClass = "org.gradle.sample.transform.javamodules.ExtraModuleInfoPlugin"
|
||||
}
|
||||
register("org-openjfx-javafxplugin") {
|
||||
id = "org-openjfx-javafxplugin"
|
||||
implementationClass = "org.openjfx.gradle.JavaFXPlugin"
|
||||
}
|
||||
}
|
||||
}
|
||||
-54
@@ -1,54 +0,0 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.attributes.Attribute;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
|
||||
/**
|
||||
* Entry point of our plugin that should be applied in the root project.
|
||||
*/
|
||||
public class ExtraModuleInfoPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
// register the plugin extension as 'extraJavaModuleInfo {}' configuration block
|
||||
ExtraModuleInfoPluginExtension extension = project.getObjects().newInstance(ExtraModuleInfoPluginExtension.class);
|
||||
project.getExtensions().add(ExtraModuleInfoPluginExtension.class, "extraJavaModuleInfo", extension);
|
||||
|
||||
// setup the transform for all projects in the build
|
||||
project.getPlugins().withType(JavaPlugin.class).configureEach(javaPlugin -> configureTransform(project, extension));
|
||||
}
|
||||
|
||||
private void configureTransform(Project project, ExtraModuleInfoPluginExtension extension) {
|
||||
Attribute<String> artifactType = Attribute.of("artifactType", String.class);
|
||||
Attribute<Boolean> javaModule = Attribute.of("javaModule", Boolean.class);
|
||||
|
||||
// compile and runtime classpath express that they only accept modules by requesting the javaModule=true attribute
|
||||
project.getConfigurations().matching(this::isResolvingJavaPluginConfiguration).all(
|
||||
c -> c.getAttributes().attribute(javaModule, true));
|
||||
|
||||
// all Jars have a javaModule=false attribute by default; the transform also recognizes modules and returns them without modification
|
||||
project.getDependencies().getArtifactTypes().getByName("jar").getAttributes().attribute(javaModule, false);
|
||||
|
||||
// register the transform for Jars and "javaModule=false -> javaModule=true"; the plugin extension object fills the input parameter
|
||||
project.getDependencies().registerTransform(ExtraModuleInfoTransform.class, t -> {
|
||||
t.parameters(p -> {
|
||||
p.setModuleInfo(extension.getModuleInfo());
|
||||
p.setAutomaticModules(extension.getAutomaticModules());
|
||||
});
|
||||
t.getFrom().attribute(artifactType, "jar").attribute(javaModule, false);
|
||||
t.getTo().attribute(artifactType, "jar").attribute(javaModule, true);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isResolvingJavaPluginConfiguration(Configuration configuration) {
|
||||
if (!configuration.isCanBeResolved()) {
|
||||
return false;
|
||||
}
|
||||
return configuration.getName().endsWith(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
||||
|| configuration.getName().endsWith(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME.substring(1))
|
||||
|| configuration.getName().endsWith(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.substring(1));
|
||||
}
|
||||
}
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
|
||||
import org.gradle.api.Action;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A data class to collect all the module information we want to add.
|
||||
* Here the class is used as extension that can be configured in the build script
|
||||
* and as input to the ExtraModuleInfoTransform that add the information to Jars.
|
||||
*/
|
||||
public class ExtraModuleInfoPluginExtension {
|
||||
|
||||
private final Map<String, ModuleInfo> moduleInfo = new HashMap<>();
|
||||
private final Map<String, String> automaticModules = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Add full module information for a given Jar file.
|
||||
*/
|
||||
public void module(String jarName, String moduleName, String moduleVersion) {
|
||||
module(jarName, moduleName, moduleVersion, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add full module information, including exported packages and dependencies, for a given Jar file.
|
||||
*/
|
||||
public void module(String jarName, String moduleName, String moduleVersion, @Nullable Action<? super ModuleInfo> conf) {
|
||||
ModuleInfo moduleInfo = new ModuleInfo(moduleName, moduleVersion);
|
||||
if (conf != null) {
|
||||
conf.execute(moduleInfo);
|
||||
}
|
||||
this.moduleInfo.put(jarName, moduleInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add only an automatic module name to a given jar file.
|
||||
*/
|
||||
public void automaticModule(String jarName, String moduleName) {
|
||||
automaticModules.put(jarName, moduleName);
|
||||
}
|
||||
|
||||
protected Map<String, ModuleInfo> getModuleInfo() {
|
||||
return moduleInfo;
|
||||
}
|
||||
|
||||
protected Map<String, String> getAutomaticModules() {
|
||||
return automaticModules;
|
||||
}
|
||||
}
|
||||
-176
@@ -1,176 +0,0 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import org.gradle.api.artifacts.transform.InputArtifact;
|
||||
import org.gradle.api.artifacts.transform.TransformAction;
|
||||
import org.gradle.api.artifacts.transform.TransformOutputs;
|
||||
import org.gradle.api.artifacts.transform.TransformParameters;
|
||||
import org.gradle.api.file.FileSystemLocation;
|
||||
import org.gradle.api.provider.Provider;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.ModuleVisitor;
|
||||
import org.objectweb.asm.Opcodes;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.jar.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
/**
|
||||
* An artifact transform that applies additional information to Jars without module information.
|
||||
* The transformation fails the build if a Jar does not contain information and no extra information
|
||||
* was defined for it. This way we make sure that all Jars are turned into modules.
|
||||
*/
|
||||
abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraModuleInfoTransform.Parameter> {
|
||||
|
||||
public static class Parameter implements TransformParameters, Serializable {
|
||||
private Map<String, ModuleInfo> moduleInfo = Collections.emptyMap();
|
||||
private Map<String, String> automaticModules = Collections.emptyMap();
|
||||
|
||||
@Input
|
||||
public Map<String, ModuleInfo> getModuleInfo() {
|
||||
return moduleInfo;
|
||||
}
|
||||
|
||||
@Input
|
||||
public Map<String, String> getAutomaticModules() {
|
||||
return automaticModules;
|
||||
}
|
||||
|
||||
public void setModuleInfo(Map<String, ModuleInfo> moduleInfo) {
|
||||
this.moduleInfo = moduleInfo;
|
||||
}
|
||||
|
||||
public void setAutomaticModules(Map<String, String> automaticModules) {
|
||||
this.automaticModules = automaticModules;
|
||||
}
|
||||
}
|
||||
|
||||
@InputArtifact
|
||||
protected abstract Provider<FileSystemLocation> getInputArtifact();
|
||||
|
||||
@Override
|
||||
public void transform(TransformOutputs outputs) {
|
||||
Map<String, ModuleInfo> moduleInfo = getParameters().moduleInfo;
|
||||
Map<String, String> automaticModules = getParameters().automaticModules;
|
||||
File originalJar = getInputArtifact().get().getAsFile();
|
||||
String originalJarName = originalJar.getName();
|
||||
|
||||
//Recreate jackson jars as open, non-synthetic modules
|
||||
if ((isModule(originalJar) && !originalJarName.contains("jackson")) || originalJarName.startsWith("javafx-")) {
|
||||
outputs.file(originalJar);
|
||||
} else if (moduleInfo.containsKey(originalJarName)) {
|
||||
addModuleDescriptor(originalJar, getModuleJar(outputs, originalJar), moduleInfo.get(originalJarName));
|
||||
} else if (isAutoModule(originalJar)) {
|
||||
outputs.file(originalJar);
|
||||
} else if (automaticModules.containsKey(originalJarName)) {
|
||||
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
|
||||
} else if(originalJarName.startsWith("kotlin-stdlib-common")) {
|
||||
//ignore
|
||||
} else {
|
||||
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isModule(File jar) {
|
||||
Pattern moduleInfoClassMrjarPath = Pattern.compile("META-INF/versions/\\d+/module-info.class");
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
|
||||
boolean isMultiReleaseJar = containsMultiReleaseJarEntry(inputStream);
|
||||
ZipEntry next = inputStream.getNextEntry();
|
||||
while (next != null) {
|
||||
if ("module-info.class".equals(next.getName())) {
|
||||
return true;
|
||||
}
|
||||
if (isMultiReleaseJar && moduleInfoClassMrjarPath.matcher(next.getName()).matches()) {
|
||||
return true;
|
||||
}
|
||||
next = inputStream.getNextEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean containsMultiReleaseJarEntry(JarInputStream jarStream) {
|
||||
Manifest manifest = jarStream.getManifest();
|
||||
return manifest != null && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"));
|
||||
}
|
||||
|
||||
private boolean isAutoModule(File jar) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(jar))) {
|
||||
return inputStream.getManifest().getMainAttributes().getValue("Automatic-Module-Name") != null;
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private File getModuleJar(TransformOutputs outputs, File originalJar) {
|
||||
return outputs.file(originalJar.getName().substring(0, originalJar.getName().lastIndexOf('.')) + "-module.jar");
|
||||
}
|
||||
|
||||
private static void addAutomaticModuleName(File originalJar, File moduleJar, String moduleName) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
|
||||
Manifest manifest = inputStream.getManifest();
|
||||
manifest.getMainAttributes().put(new Attributes.Name("Automatic-Module-Name"), moduleName);
|
||||
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), inputStream.getManifest())) {
|
||||
copyEntries(inputStream, outputStream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addModuleDescriptor(File originalJar, File moduleJar, ModuleInfo moduleInfo) {
|
||||
try (JarInputStream inputStream = new JarInputStream(new FileInputStream(originalJar))) {
|
||||
Manifest manifest = inputStream.getManifest();
|
||||
if(manifest == null) {
|
||||
manifest = new Manifest();
|
||||
}
|
||||
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(moduleJar), manifest)) {
|
||||
copyEntries(inputStream, outputStream);
|
||||
outputStream.putNextEntry(new JarEntry("module-info.class"));
|
||||
outputStream.write(addModuleInfo(moduleInfo));
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyEntries(JarInputStream inputStream, JarOutputStream outputStream) throws IOException {
|
||||
JarEntry jarEntry = inputStream.getNextJarEntry();
|
||||
while (jarEntry != null) {
|
||||
if(!jarEntry.getName().equals("module-info.class")) {
|
||||
outputStream.putNextEntry(jarEntry);
|
||||
outputStream.write(inputStream.readAllBytes());
|
||||
outputStream.closeEntry();
|
||||
}
|
||||
jarEntry = inputStream.getNextJarEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] addModuleInfo(ModuleInfo moduleInfo) {
|
||||
ClassWriter classWriter = new ClassWriter(0);
|
||||
classWriter.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null);
|
||||
ModuleVisitor moduleVisitor = classWriter.visitModule(moduleInfo.getModuleName(), Opcodes.ACC_OPEN, moduleInfo.getModuleVersion());
|
||||
for (String packageName : moduleInfo.getExports()) {
|
||||
moduleVisitor.visitExport(packageName.replace('.', '/'), 0);
|
||||
}
|
||||
moduleVisitor.visitRequire("java.base", 0, null);
|
||||
for (String requireName : moduleInfo.getRequires()) {
|
||||
moduleVisitor.visitRequire(requireName, 0, null);
|
||||
}
|
||||
for (String requireName : moduleInfo.getRequiresTransitive()) {
|
||||
moduleVisitor.visitRequire(requireName, Opcodes.ACC_TRANSITIVE, null);
|
||||
}
|
||||
for (String usesName : moduleInfo.getUses()) {
|
||||
moduleVisitor.visitUse(usesName.replace('.', '/'));
|
||||
}
|
||||
moduleVisitor.visitEnd();
|
||||
classWriter.visitEnd();
|
||||
return classWriter.toByteArray();
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package org.gradle.sample.transform.javamodules;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Data class to hold the information that should be added as module-info.class to an existing Jar file.
|
||||
*/
|
||||
public class ModuleInfo implements Serializable {
|
||||
private String moduleName;
|
||||
private String moduleVersion;
|
||||
private List<String> exports = new ArrayList<>();
|
||||
private List<String> requires = new ArrayList<>();
|
||||
private List<String> requiresTransitive = new ArrayList<>();
|
||||
private List<String> uses = new ArrayList<>();
|
||||
|
||||
ModuleInfo(String moduleName, String moduleVersion) {
|
||||
this.moduleName = moduleName;
|
||||
this.moduleVersion = moduleVersion;
|
||||
}
|
||||
|
||||
public void exports(String exports) {
|
||||
this.exports.add(exports);
|
||||
}
|
||||
|
||||
public void requires(String requires) {
|
||||
this.requires.add(requires);
|
||||
}
|
||||
|
||||
public void requiresTransitive(String requiresTransitive) {
|
||||
this.requiresTransitive.add(requiresTransitive);
|
||||
}
|
||||
|
||||
public void uses(String uses) {
|
||||
this.uses.add(uses);
|
||||
}
|
||||
|
||||
public String getModuleName() {
|
||||
return moduleName;
|
||||
}
|
||||
|
||||
protected String getModuleVersion() {
|
||||
return moduleVersion;
|
||||
}
|
||||
|
||||
protected List<String> getExports() {
|
||||
return exports;
|
||||
}
|
||||
|
||||
protected List<String> getRequires() {
|
||||
return requires;
|
||||
}
|
||||
|
||||
protected List<String> getRequiresTransitive() {
|
||||
return requiresTransitive;
|
||||
}
|
||||
|
||||
protected List<String> getUses() {
|
||||
return uses;
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018, 2020, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import org.gradle.api.GradleException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public enum JavaFXModule {
|
||||
|
||||
BASE,
|
||||
GRAPHICS(BASE),
|
||||
CONTROLS(BASE, GRAPHICS),
|
||||
FXML(BASE, GRAPHICS),
|
||||
MEDIA(BASE, GRAPHICS),
|
||||
SWING(BASE, GRAPHICS),
|
||||
WEB(BASE, CONTROLS, GRAPHICS, MEDIA);
|
||||
|
||||
static final String PREFIX_MODULE = "javafx.";
|
||||
private static final String PREFIX_ARTIFACT = "javafx-";
|
||||
|
||||
private List<JavaFXModule> dependentModules;
|
||||
|
||||
JavaFXModule(JavaFXModule...dependentModules) {
|
||||
this.dependentModules = List.of(dependentModules);
|
||||
}
|
||||
|
||||
public static Optional<JavaFXModule> fromModuleName(String moduleName) {
|
||||
return Stream.of(JavaFXModule.values())
|
||||
.filter(javaFXModule -> moduleName.equals(javaFXModule.getModuleName()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
public String getModuleName() {
|
||||
return PREFIX_MODULE + name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public String getModuleJarFileName() {
|
||||
return getModuleName() + ".jar";
|
||||
}
|
||||
|
||||
public String getArtifactName() {
|
||||
return PREFIX_ARTIFACT + name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
public boolean compareJarFileName(JavaFXPlatform platform, String jarFileName) {
|
||||
Pattern p = Pattern.compile(getArtifactName() + "-.+-" + platform.getClassifier() + "\\.jar");
|
||||
return p.matcher(jarFileName).matches();
|
||||
}
|
||||
|
||||
public static Set<JavaFXModule> getJavaFXModules(List<String> moduleNames) {
|
||||
validateModules(moduleNames);
|
||||
|
||||
return moduleNames.stream()
|
||||
.map(JavaFXModule::fromModuleName)
|
||||
.flatMap(Optional::stream)
|
||||
.flatMap(javaFXModule -> javaFXModule.getMavenDependencies().stream())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public static void validateModules(List<String> moduleNames) {
|
||||
var invalidModules = moduleNames.stream()
|
||||
.filter(module -> JavaFXModule.fromModuleName(module).isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (! invalidModules.isEmpty()) {
|
||||
throw new GradleException("Found one or more invalid JavaFX module names: " + invalidModules);
|
||||
}
|
||||
}
|
||||
|
||||
public List<JavaFXModule> getDependentModules() {
|
||||
return dependentModules;
|
||||
}
|
||||
|
||||
public List<JavaFXModule> getMavenDependencies() {
|
||||
List<JavaFXModule> dependencies = new ArrayList<>(dependentModules);
|
||||
dependencies.add(0, this);
|
||||
return dependencies;
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.repositories.FlatDirectoryArtifactRepository;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.openjfx.gradle.JavaFXModule.PREFIX_MODULE;
|
||||
|
||||
public class JavaFXOptions {
|
||||
|
||||
private static final String MAVEN_JAVAFX_ARTIFACT_GROUP_ID = "org.openjfx";
|
||||
private static final String JAVAFX_SDK_LIB_FOLDER = "lib";
|
||||
|
||||
private final Project project;
|
||||
private final JavaFXPlatform platform;
|
||||
|
||||
private String version = "16";
|
||||
private String sdk;
|
||||
private String configuration = "implementation";
|
||||
private String lastUpdatedConfiguration;
|
||||
private List<String> modules = new ArrayList<>();
|
||||
private FlatDirectoryArtifactRepository customSDKArtifactRepository;
|
||||
|
||||
public JavaFXOptions(Project project) {
|
||||
this.project = project;
|
||||
this.platform = JavaFXPlatform.detect(project);
|
||||
}
|
||||
|
||||
public JavaFXPlatform getPlatform() {
|
||||
return platform;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(String version) {
|
||||
this.version = version;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
/**
|
||||
* If set, the JavaFX modules will be taken from this local
|
||||
* repository, and not from Maven Central
|
||||
* @param sdk, the path to the local JavaFX SDK folder
|
||||
*/
|
||||
public void setSdk(String sdk) {
|
||||
this.sdk = sdk;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
public String getSdk() {
|
||||
return sdk;
|
||||
}
|
||||
|
||||
/** Set the configuration name for dependencies, e.g.
|
||||
* 'implementation', 'compileOnly' etc.
|
||||
* @param configuration The configuration name for dependencies
|
||||
*/
|
||||
public void setConfiguration(String configuration) {
|
||||
this.configuration = configuration;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
public String getConfiguration() {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public List<String> getModules() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
public void setModules(List<String> modules) {
|
||||
this.modules = modules;
|
||||
updateJavaFXDependencies();
|
||||
}
|
||||
|
||||
public void modules(String...moduleNames) {
|
||||
setModules(List.of(moduleNames));
|
||||
}
|
||||
|
||||
private void updateJavaFXDependencies() {
|
||||
clearJavaFXDependencies();
|
||||
|
||||
String configuration = getConfiguration();
|
||||
JavaFXModule.getJavaFXModules(this.modules).stream()
|
||||
.sorted()
|
||||
.forEach(javaFXModule -> {
|
||||
if (customSDKArtifactRepository != null) {
|
||||
project.getDependencies().add(configuration, Map.of("name", javaFXModule.getModuleName()));
|
||||
} else {
|
||||
project.getDependencies().add(configuration,
|
||||
String.format("%s:%s:%s:%s", MAVEN_JAVAFX_ARTIFACT_GROUP_ID, javaFXModule.getArtifactName(),
|
||||
getVersion(), getPlatform().getClassifier()));
|
||||
}
|
||||
});
|
||||
lastUpdatedConfiguration = configuration;
|
||||
}
|
||||
|
||||
private void clearJavaFXDependencies() {
|
||||
if (customSDKArtifactRepository != null) {
|
||||
project.getRepositories().remove(customSDKArtifactRepository);
|
||||
customSDKArtifactRepository = null;
|
||||
}
|
||||
|
||||
if (sdk != null && ! sdk.isEmpty()) {
|
||||
Map<String, String> dirs = new HashMap<>();
|
||||
dirs.put("name", "customSDKArtifactRepository");
|
||||
if (sdk.endsWith(File.separator)) {
|
||||
dirs.put("dirs", sdk + JAVAFX_SDK_LIB_FOLDER);
|
||||
} else {
|
||||
dirs.put("dirs", sdk + File.separator + JAVAFX_SDK_LIB_FOLDER);
|
||||
}
|
||||
customSDKArtifactRepository = project.getRepositories().flatDir(dirs);
|
||||
}
|
||||
|
||||
if (lastUpdatedConfiguration == null) {
|
||||
return;
|
||||
}
|
||||
var configuration = project.getConfigurations().findByName(lastUpdatedConfiguration);
|
||||
if (configuration != null) {
|
||||
if (customSDKArtifactRepository != null) {
|
||||
configuration.getDependencies()
|
||||
.removeIf(dependency -> dependency.getName().startsWith(PREFIX_MODULE));
|
||||
}
|
||||
configuration.getDependencies()
|
||||
.removeIf(dependency -> MAVEN_JAVAFX_ARTIFACT_GROUP_ID.equals(dependency.getGroup()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import com.google.gradle.osdetector.OsDetector;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Project;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public enum JavaFXPlatform {
|
||||
|
||||
LINUX("linux", "linux-x86_64"),
|
||||
LINUX_MONOCLE("linux-monocle", "linux-x86_64-monocle"),
|
||||
LINUX_AARCH64("linux-aarch64", "linux-aarch_64"),
|
||||
LINUX_AARCH64_MONOCLE("linux-aarch64-monocle", "linux-aarch_64-monocle"),
|
||||
WINDOWS("win", "windows-x86_64"),
|
||||
WINDOWS_MONOCLE("win-monocle", "windows-x86_64-monocle"),
|
||||
OSX("mac", "osx-x86_64"),
|
||||
OSX_MONOCLE("mac-monocle", "osx-x86_64-monocle"),
|
||||
OSX_AARCH64("mac-aarch64", "osx-aarch_64"),
|
||||
OSX_AARCH64_MONOCLE("mac-aarch64-monocle", "osx-aarch_64-monocle");
|
||||
|
||||
private final String classifier;
|
||||
private final String osDetectorClassifier;
|
||||
|
||||
JavaFXPlatform( String classifier, String osDetectorClassifier ) {
|
||||
this.classifier = classifier;
|
||||
this.osDetectorClassifier = osDetectorClassifier;
|
||||
}
|
||||
|
||||
public String getClassifier() {
|
||||
return classifier;
|
||||
}
|
||||
|
||||
public static JavaFXPlatform detect(Project project) {
|
||||
|
||||
String osClassifier = project.getExtensions().getByType(OsDetector.class).getClassifier();
|
||||
|
||||
if("true".equals(System.getProperty("java.awt.headless"))) {
|
||||
osClassifier += "-monocle";
|
||||
}
|
||||
|
||||
for ( JavaFXPlatform platform: values()) {
|
||||
if ( platform.osDetectorClassifier.equals(osClassifier)) {
|
||||
return platform;
|
||||
}
|
||||
}
|
||||
|
||||
String supportedPlatforms = Arrays.stream(values())
|
||||
.map(p->p.osDetectorClassifier)
|
||||
.collect(Collectors.joining("', '", "'", "'"));
|
||||
|
||||
throw new GradleException(
|
||||
String.format(
|
||||
"Unsupported JavaFX platform found: '%s'! " +
|
||||
"This plugin is designed to work on supported platforms only." +
|
||||
"Current supported platforms are %s.", osClassifier, supportedPlatforms )
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2018, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle;
|
||||
|
||||
import com.google.gradle.osdetector.OsDetectorPlugin;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.javamodularity.moduleplugin.ModuleSystemPlugin;
|
||||
import org.openjfx.gradle.tasks.ExecTask;
|
||||
|
||||
public class JavaFXPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().apply(OsDetectorPlugin.class);
|
||||
project.getPlugins().apply(ModuleSystemPlugin.class);
|
||||
|
||||
project.getExtensions().create("javafx", JavaFXOptions.class, project);
|
||||
|
||||
project.getTasks().create("configJavafxRun", ExecTask.class, project);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019, 2021, Gluon
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of the copyright holder nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package org.openjfx.gradle.tasks;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.logging.Logger;
|
||||
import org.gradle.api.logging.Logging;
|
||||
import org.gradle.api.plugins.ApplicationPlugin;
|
||||
import org.gradle.api.tasks.JavaExec;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.javamodularity.moduleplugin.extensions.RunModuleOptions;
|
||||
import org.openjfx.gradle.JavaFXModule;
|
||||
import org.openjfx.gradle.JavaFXOptions;
|
||||
import org.openjfx.gradle.JavaFXPlatform;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class ExecTask extends DefaultTask {
|
||||
|
||||
private static final Logger LOGGER = Logging.getLogger(ExecTask.class);
|
||||
|
||||
private final Project project;
|
||||
private JavaExec execTask;
|
||||
|
||||
@Inject
|
||||
public ExecTask(Project project) {
|
||||
this.project = project;
|
||||
project.getPluginManager().withPlugin(ApplicationPlugin.APPLICATION_PLUGIN_NAME, e -> {
|
||||
execTask = (JavaExec) project.getTasks().findByName(ApplicationPlugin.TASK_RUN_NAME);
|
||||
if (execTask != null) {
|
||||
execTask.dependsOn(this);
|
||||
} else {
|
||||
throw new GradleException("Run task not found.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void action() {
|
||||
if (execTask != null) {
|
||||
JavaFXOptions javaFXOptions = project.getExtensions().getByType(JavaFXOptions.class);
|
||||
JavaFXModule.validateModules(javaFXOptions.getModules());
|
||||
|
||||
var definedJavaFXModuleNames = new TreeSet<>(javaFXOptions.getModules());
|
||||
if (!definedJavaFXModuleNames.isEmpty()) {
|
||||
RunModuleOptions moduleOptions = execTask.getExtensions().findByType(RunModuleOptions.class);
|
||||
|
||||
final FileCollection classpathWithoutJavaFXJars = execTask.getClasspath().filter(
|
||||
jar -> Arrays.stream(JavaFXModule.values()).noneMatch(javaFXModule -> jar.getName().contains(javaFXModule.getArtifactName()))
|
||||
);
|
||||
final FileCollection javaFXPlatformJars = execTask.getClasspath().filter(jar -> isJavaFXJar(jar, javaFXOptions.getPlatform()));
|
||||
|
||||
if (moduleOptions != null) {
|
||||
LOGGER.info("Modular JavaFX application found");
|
||||
// Remove empty JavaFX jars from classpath
|
||||
execTask.setClasspath(classpathWithoutJavaFXJars.plus(javaFXPlatformJars));
|
||||
definedJavaFXModuleNames.forEach(javaFXModule -> moduleOptions.getAddModules().add(javaFXModule));
|
||||
} else {
|
||||
LOGGER.info("Non-modular JavaFX application found");
|
||||
// Remove all JavaFX jars from classpath
|
||||
execTask.setClasspath(classpathWithoutJavaFXJars);
|
||||
|
||||
var javaFXModuleJvmArgs = List.of("--module-path", javaFXPlatformJars.getAsPath());
|
||||
|
||||
var jvmArgs = new ArrayList<String>();
|
||||
jvmArgs.add("--add-modules");
|
||||
jvmArgs.add(String.join(",", definedJavaFXModuleNames));
|
||||
|
||||
List<String> execJvmArgs = execTask.getJvmArgs();
|
||||
if (execJvmArgs != null) {
|
||||
jvmArgs.addAll(execJvmArgs);
|
||||
}
|
||||
jvmArgs.addAll(javaFXModuleJvmArgs);
|
||||
|
||||
execTask.setJvmArgs(jvmArgs);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new GradleException("Run task not found. Please, make sure the Application plugin is applied");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isJavaFXJar(File jar, JavaFXPlatform platform) {
|
||||
return jar.isFile() &&
|
||||
Arrays.stream(JavaFXModule.values()).anyMatch(javaFXModule ->
|
||||
javaFXModule.compareJarFileName(platform, jar.getName()) ||
|
||||
javaFXModule.getModuleJarFileName().equals(jar.getName()));
|
||||
}
|
||||
}
|
||||
+20
-46
@@ -12,62 +12,36 @@ Work on resolving both of these issues is ongoing.
|
||||
### Install Java
|
||||
|
||||
Because Sparrow bundles a Java runtime in the release binaries, it is essential to have the same version of Java installed when creating the release.
|
||||
For v1.6.6 and later, this is Eclipse Temurin 18.0.1+10.
|
||||
For v1.6.6 to v1.9.1, this was Eclipse Temurin 18.0.1+10. For v2.0.0 to v2.3.1, this was Eclipse Temurin 22.0.2+9. For v2.4.0 and later, Eclipse Temurin 25.0.2+10 is used.
|
||||
|
||||
Note: Do not install Java using a system package manager (e.g. apt, dnf, rpm).
|
||||
Linux packages replace the JDK's bundled `cacerts` file with a symlink to the system CA certificates, which differ from those in the release tarballs and will produce a non-reproducible build.
|
||||
|
||||
#### Java from Adoptium github repo
|
||||
|
||||
It is available for all supported platforms from [Eclipse Temurin 18.0.1+10](https://github.com/adoptium/temurin18-binaries/releases/tag/jdk-18.0.1%2B10).
|
||||
It is available for all supported platforms from [Eclipse Temurin 25.0.2+10](https://github.com/adoptium/temurin25-binaries/releases/tag/jdk-25.0.2%2B10).
|
||||
|
||||
For reference, the downloads are as follows:
|
||||
- [Linux x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_linux_hotspot_18.0.1_10.tar.gz)
|
||||
- [MacOS x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_mac_hotspot_18.0.1_10.tar.gz)
|
||||
- [MacOS aarch64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_aarch64_mac_hotspot_18.0.1_10.tar.gz)
|
||||
- [Windows x64](https://github.com/adoptium/temurin18-binaries/releases/download/jdk-18.0.1%2B10/OpenJDK18U-jdk_x64_windows_hotspot_18.0.1_10.zip)
|
||||
- [Linux x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz)
|
||||
- [Linux aarch64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_linux_hotspot_25.0.2_10.tar.gz)
|
||||
- [MacOS x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_mac_hotspot_25.0.2_10.tar.gz)
|
||||
- [MacOS aarch64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_aarch64_mac_hotspot_25.0.2_10.tar.gz)
|
||||
- [Windows x64](https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.2%2B10/OpenJDK25U-jdk_x64_windows_hotspot_25.0.2_10.zip)
|
||||
|
||||
#### Java from Adoptium deb repo
|
||||
|
||||
It is also possible to install via a package manager on *nix systems. For example, on Debian/Ubuntu systems:
|
||||
|
||||
- Install dependencies:
|
||||
```sh
|
||||
sudo apt-get install -y wget curl apt-transport-https gnupg
|
||||
On Linux, extract the tarball and set `JAVA_HOME` to use it for the build:
|
||||
```shell
|
||||
tar -xzf OpenJDK25U-jdk_x64_linux_hotspot_25.0.2_10.tar.gz
|
||||
export JAVA_HOME=$PWD/jdk-25.0.2+10
|
||||
export PATH=$JAVA_HOME/bin:$PATH
|
||||
```
|
||||
|
||||
Download Adoptium public PGP key:
|
||||
```sh
|
||||
curl --tlsv1.2 --proto =https --location -o adoptium.asc https://packages.adoptium.net/artifactory/api/gpg/key/public
|
||||
```
|
||||
#### Java from SDKMAN
|
||||
|
||||
Check if key fingerprint matches: `3B04D753C9050D9A5D343F39843C48A565F8F04B`:
|
||||
```
|
||||
gpg --import --import-options show-only adoptium.asc
|
||||
```
|
||||
If key doesn't match, do not procede.
|
||||
|
||||
Add Adoptium PGP key to a the keyring shared folder:
|
||||
```sh
|
||||
sudo cp adoptium.asc /usr/share/keyrings/
|
||||
```
|
||||
|
||||
Add Adoptium debian repository:
|
||||
```sh
|
||||
echo "deb [signed-by=/usr/share/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
|
||||
```
|
||||
|
||||
Update cache, install the desired temurin version and configure java to be linked to this same version:
|
||||
```
|
||||
sudo apt update -y
|
||||
sudo apt-get install -y temurin-18-jdk=18.0.1+10
|
||||
sudo update-alternatives --config java
|
||||
```
|
||||
|
||||
#### Java from SDK
|
||||
|
||||
A alternative option for all platforms is to use the [sdkman.io](https://sdkman.io/) package manager ([Git Bash for Windows](https://git-scm.com/download/win) is a good choice on that platform).
|
||||
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.7.9"
|
||||
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:
|
||||
|
||||
|
||||
+1
-1
Submodule drongo updated: 28551247c6...fa1bb4eec2
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Vendored
BIN
Binary file not shown.
+2
-1
@@ -1,6 +1,7 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -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/.
|
||||
@@ -83,10 +85,8 @@ done
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
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
|
||||
@@ -114,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.
|
||||
@@ -133,10 +132,13 @@ location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
@@ -144,7 +146,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
@@ -152,7 +154,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
@@ -169,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" )
|
||||
|
||||
@@ -197,16 +198,19 @@ if "$cygwin" || "$msys" ; then
|
||||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
||||
Vendored
+13
-12
@@ -13,6 +13,8 @@
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@@ -43,11 +45,11 @@ set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
@@ -57,22 +59,21 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
||||
Submodule
+1
Submodule lark added at 00fdebc715
Executable
+48
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
set -e # Exit on any error
|
||||
|
||||
# Define paths
|
||||
BUILD_DIR="build"
|
||||
JPACKAGE_DIR="$BUILD_DIR/jpackage"
|
||||
TEMP_DIR="$BUILD_DIR/repackage"
|
||||
|
||||
# Find the .deb file in build/jpackage (assuming there is only one)
|
||||
DEB_FILE=$(find "$JPACKAGE_DIR" -type f -name "*.deb" -print -quit)
|
||||
|
||||
# Check if a .deb file was found
|
||||
if [ -z "$DEB_FILE" ]; then
|
||||
echo "Error: No .deb file found in $JPACKAGE_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract the filename from the path for later use
|
||||
DEB_FILENAME=$(basename "$DEB_FILE")
|
||||
|
||||
echo "Found .deb file: $DEB_FILENAME"
|
||||
|
||||
# Create a temp directory inside build to avoid file conflicts
|
||||
mkdir -p "$TEMP_DIR"
|
||||
cd "$TEMP_DIR"
|
||||
|
||||
# Extract the .deb file contents
|
||||
ar x "../../$DEB_FILE"
|
||||
|
||||
# Decompress zst files to tar
|
||||
unzstd control.tar.zst
|
||||
unzstd data.tar.zst
|
||||
|
||||
# Compress tar files to xz
|
||||
xz -c control.tar > control.tar.xz
|
||||
xz -c data.tar > data.tar.xz
|
||||
|
||||
# Remove the original .deb file
|
||||
rm "../../$DEB_FILE"
|
||||
|
||||
# Create the new .deb file with xz compression in the original location
|
||||
ar cr "../../$DEB_FILE" debian-binary control.tar.xz data.tar.xz
|
||||
|
||||
# Clean up temp files
|
||||
cd ../..
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
echo "Repackaging complete: $DEB_FILENAME"
|
||||
@@ -1,3 +1,4 @@
|
||||
rootProject.name = 'sparrow'
|
||||
include 'drongo'
|
||||
include 'lark'
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
mime-type=application/pgp-signature
|
||||
extension=asc
|
||||
description=ASCII Armored File
|
||||
@@ -1,2 +0,0 @@
|
||||
mime-type=x-scheme-handler/auth47
|
||||
description=Auth47 Authentication URI
|
||||
@@ -1,2 +0,0 @@
|
||||
mime-type=x-scheme-handler/bitcoin
|
||||
description=Bitcoin Scheme URI
|
||||
@@ -1,2 +0,0 @@
|
||||
mime-type=x-scheme-handler/lightning
|
||||
description=LNURL URI
|
||||
@@ -0,0 +1,12 @@
|
||||
Package: sparrowserver
|
||||
Version: ${version}-1
|
||||
Section: utils
|
||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||
Priority: optional
|
||||
Architecture: ${arch}
|
||||
Conflicts: sparrow (<= 2.1.4)
|
||||
Replaces: sparrow (<= 2.1.4)
|
||||
Provides: sparrowserver
|
||||
Description: Sparrow Server
|
||||
Depends: libc6, zlib1g
|
||||
Installed-Size: ${size}
|
||||
@@ -0,0 +1,85 @@
|
||||
Summary: Sparrow Server
|
||||
Name: sparrowserver
|
||||
Version: ${version}
|
||||
Release: 1
|
||||
License: ASL 2.0
|
||||
Vendor: Unknown
|
||||
|
||||
%if "x" != "x"
|
||||
URL: https://sparrowwallet.com
|
||||
%endif
|
||||
|
||||
%if "x/opt" != "x"
|
||||
Prefix: /opt
|
||||
%endif
|
||||
|
||||
Provides: sparrowserver
|
||||
Obsoletes: sparrow <= 2.1.4
|
||||
|
||||
%if "xutils" != "x"
|
||||
Group: utils
|
||||
%endif
|
||||
|
||||
Autoprov: 0
|
||||
Autoreq: 0
|
||||
|
||||
#comment line below to enable effective jar compression
|
||||
#it could easily get your package size from 40 to 15Mb but
|
||||
#build time will substantially increase and it may require unpack200/system java to install
|
||||
%define __jar_repack %{nil}
|
||||
|
||||
# on RHEL we got unwanted improved debugging enhancements
|
||||
%define _build_id_links none
|
||||
|
||||
%define package_filelist %{_builddir}/%{name}.files
|
||||
%define app_filelist %{_builddir}/%{name}.app.files
|
||||
%define filesystem_filelist %{_builddir}/%{name}.filesystem.files
|
||||
|
||||
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
|
||||
|
||||
%description
|
||||
Sparrow Server
|
||||
|
||||
%global __os_install_post %{nil}
|
||||
|
||||
%prep
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
install -d -m 755 %{buildroot}/opt/sparrowserver
|
||||
cp -r %{_sourcedir}/opt/sparrowserver/* %{buildroot}/opt/sparrowserver
|
||||
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
||||
install -d -m 755 %{buildroot}/lib/systemd/system
|
||||
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
||||
fi
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
%define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE}
|
||||
install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}"
|
||||
install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}"
|
||||
%endif
|
||||
(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist}
|
||||
{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist}
|
||||
comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist}
|
||||
sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
||||
(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist}
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist}
|
||||
%endif
|
||||
|
||||
%files -f %{package_filelist}
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
%license "%{license_install_file}"
|
||||
%endif
|
||||
|
||||
%post
|
||||
package_type=rpm
|
||||
|
||||
%pre
|
||||
package_type=rpm
|
||||
|
||||
%preun
|
||||
package_type=rpm
|
||||
|
||||
%clean
|
||||
@@ -1,10 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Name=Sparrow
|
||||
Comment=Sparrow
|
||||
Exec=/opt/sparrow/bin/Sparrow %U
|
||||
Icon=/opt/sparrow/lib/Sparrow.png
|
||||
Exec=/opt/sparrowwallet/bin/Sparrow %U
|
||||
Icon=/opt/sparrowwallet/lib/Sparrow.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Finance;Network;
|
||||
MimeType=application/psbt;application/bitcoin-transaction;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||
MimeType=application/psbt;application/bitcoin-transaction;application/pgp-signature;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||
StartupWMClass=Sparrow
|
||||
SingleMainWindow=true
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
Package: sparrowwallet
|
||||
Version: ${version}-1
|
||||
Section: utils
|
||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||
Priority: optional
|
||||
Architecture: ${arch}
|
||||
Provides: sparrowwallet
|
||||
Conflicts: sparrow (<= 2.1.4)
|
||||
Replaces: sparrow (<= 2.1.4)
|
||||
Description: Sparrow Wallet
|
||||
Depends: libasound2, libbsd0, libc6, libmd0, libx11-6, libxau6, libxcb1, libxdmcp6, libxext6, libxi6, libxrender1, libxtst6, xdg-utils
|
||||
Installed-Size: ${size}
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
# postinst script for sparrowwallet
|
||||
#
|
||||
# see: dh_installdeb(1)
|
||||
|
||||
set -e
|
||||
|
||||
# summary of how this script can be called:
|
||||
# * <postinst> `configure' <most-recently-configured-version>
|
||||
# * <old-postinst> `abort-upgrade' <new version>
|
||||
# * <conflictor's-postinst> `abort-remove' `in-favour' <package>
|
||||
# <new-version>
|
||||
# * <postinst> `abort-remove'
|
||||
# * <deconfigured's-postinst> `abort-deconfigure' `in-favour'
|
||||
# <failed-install-package> <version> `removing'
|
||||
# <conflicting-package> <version>
|
||||
# for details, see https://www.debian.org/doc/debian-policy/ or
|
||||
# the debian-policy package
|
||||
|
||||
package_type=deb
|
||||
|
||||
|
||||
case "$1" in
|
||||
configure)
|
||||
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
if ! getent group plugdev > /dev/null; then
|
||||
groupadd -r plugdev
|
||||
fi
|
||||
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
|
||||
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
|
||||
fi
|
||||
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
|
||||
/bin/udevadm control --reload
|
||||
/bin/udevadm trigger
|
||||
fi
|
||||
;;
|
||||
|
||||
abort-upgrade|abort-remove|abort-deconfigure)
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "postinst called with unknown argument \`$1'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||
<mime-type type="application/psbt">
|
||||
<comment>Partially Signed Bitcoin Transaction</comment>
|
||||
<glob pattern="*.psbt"/>
|
||||
</mime-type>
|
||||
<mime-type type="application/bitcoin-transaction">
|
||||
<comment>Bitcoin Transaction</comment>
|
||||
<glob pattern="*.txn"/>
|
||||
</mime-type>
|
||||
<mime-type type="application/pgp-signature">
|
||||
<comment>ASCII Armored File</comment>
|
||||
<glob pattern="*.asc"/>
|
||||
</mime-type>
|
||||
<mime-type type="x-scheme-handler/bitcoin">
|
||||
<comment>Bitcoin Scheme URI</comment>
|
||||
</mime-type>
|
||||
<mime-type type="x-scheme-handler/auth47">
|
||||
<comment>Auth47 Authentication URI</comment>
|
||||
</mime-type>
|
||||
<mime-type type="x-scheme-handler/lightning">
|
||||
<comment>LNURL URI</comment>
|
||||
</mime-type>
|
||||
</mime-info>
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
Summary: Sparrow
|
||||
Name: sparrowwallet
|
||||
Version: ${version}
|
||||
Release: 1
|
||||
License: ASL 2.0
|
||||
Vendor: Unknown
|
||||
|
||||
%if "x" != "x"
|
||||
URL: https://sparrowwallet.com
|
||||
%endif
|
||||
|
||||
%if "x/opt" != "x"
|
||||
Prefix: /opt
|
||||
%endif
|
||||
|
||||
Provides: sparrowwallet
|
||||
Obsoletes: sparrow <= 2.1.4
|
||||
|
||||
%if "xutils" != "x"
|
||||
Group: utils
|
||||
%endif
|
||||
|
||||
Autoprov: 0
|
||||
Autoreq: 0
|
||||
%if "xxdg-utils" != "x" || "x" != "x"
|
||||
Requires: xdg-utils
|
||||
%endif
|
||||
|
||||
#comment line below to enable effective jar compression
|
||||
#it could easily get your package size from 40 to 15Mb but
|
||||
#build time will substantially increase and it may require unpack200/system java to install
|
||||
%define __jar_repack %{nil}
|
||||
|
||||
# on RHEL we got unwanted improved debugging enhancements
|
||||
%define _build_id_links none
|
||||
|
||||
%define package_filelist %{_builddir}/%{name}.files
|
||||
%define app_filelist %{_builddir}/%{name}.app.files
|
||||
%define filesystem_filelist %{_builddir}/%{name}.filesystem.files
|
||||
|
||||
%define default_filesystem / /opt /usr /usr/bin /usr/lib /usr/local /usr/local/bin /usr/local/lib
|
||||
|
||||
%description
|
||||
Sparrow Wallet
|
||||
|
||||
%global __os_install_post %{nil}
|
||||
|
||||
%prep
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
rm -rf %{buildroot}
|
||||
install -d -m 755 %{buildroot}/opt/sparrowwallet
|
||||
cp -r %{_sourcedir}/opt/sparrowwallet/* %{buildroot}/opt/sparrowwallet
|
||||
if [ "$(echo %{_sourcedir}/lib/systemd/system/*.service)" != '%{_sourcedir}/lib/systemd/system/*.service' ]; then
|
||||
install -d -m 755 %{buildroot}/lib/systemd/system
|
||||
cp %{_sourcedir}/lib/systemd/system/*.service %{buildroot}/lib/systemd/system
|
||||
fi
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
%define license_install_file %{_defaultlicensedir}/%{name}-%{version}/%{basename:%{_rpmdir}/../../LICENSE}
|
||||
install -d -m 755 "%{buildroot}%{dirname:%{license_install_file}}"
|
||||
install -m 644 "%{_rpmdir}/../../LICENSE" "%{buildroot}%{license_install_file}"
|
||||
%endif
|
||||
(cd %{buildroot} && find . -path ./lib/systemd -prune -o -type d -print) | sed -e 's/^\.//' -e '/^$/d' | sort > %{app_filelist}
|
||||
{ rpm -ql filesystem || echo %{default_filesystem}; } | sort > %{filesystem_filelist}
|
||||
comm -23 %{app_filelist} %{filesystem_filelist} > %{package_filelist}
|
||||
sed -i -e 's/.*/%dir "&"/' %{package_filelist}
|
||||
(cd %{buildroot} && find . -not -type d) | sed -e 's/^\.//' -e 's/.*/"&"/' >> %{package_filelist}
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
sed -i -e 's|"%{license_install_file}"||' -e '/^$/d' %{package_filelist}
|
||||
%endif
|
||||
|
||||
%files -f %{package_filelist}
|
||||
%if "x%{_rpmdir}/../../LICENSE" != "x"
|
||||
%license "%{license_install_file}"
|
||||
%endif
|
||||
|
||||
%post
|
||||
package_type=rpm
|
||||
xdg-desktop-menu install /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||
xdg-mime install /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||
install -D -m 644 /opt/sparrowwallet/lib/runtime/conf/udev/*.rules /etc/udev/rules.d
|
||||
if ! getent group plugdev > /dev/null; then
|
||||
groupadd -r plugdev
|
||||
fi
|
||||
if ! groups "${SUDO_USER:-$(whoami)}" | grep -q plugdev; then
|
||||
usermod -aG plugdev "${SUDO_USER:-$(whoami)}"
|
||||
fi
|
||||
if [ -w /sys/devices ] && [ -w /sys/kernel/uevent_seqnum ] && [ -x /bin/udevadm ]; then
|
||||
/bin/udevadm control --reload
|
||||
/bin/udevadm trigger
|
||||
fi
|
||||
|
||||
%pre
|
||||
package_type=rpm
|
||||
file_belongs_to_single_package ()
|
||||
{
|
||||
if [ ! -e "$1" ]; then
|
||||
false
|
||||
elif [ "$package_type" = rpm ]; then
|
||||
test `rpm -q --whatprovides "$1" | wc -l` = 1
|
||||
elif [ "$package_type" = deb ]; then
|
||||
test `dpkg -S "$1" | wc -l` = 1
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
do_if_file_belongs_to_single_package ()
|
||||
{
|
||||
local file="$1"
|
||||
shift
|
||||
|
||||
if file_belongs_to_single_package "$file"; then
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$1" -gt 1 ]; then
|
||||
:;
|
||||
fi
|
||||
|
||||
%preun
|
||||
package_type=rpm
|
||||
file_belongs_to_single_package ()
|
||||
{
|
||||
if [ ! -e "$1" ]; then
|
||||
false
|
||||
elif [ "$package_type" = rpm ]; then
|
||||
test `rpm -q --whatprovides "$1" | wc -l` = 1
|
||||
elif [ "$package_type" = deb ]; then
|
||||
test `dpkg -S "$1" | wc -l` = 1
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
do_if_file_belongs_to_single_package ()
|
||||
{
|
||||
local file="$1"
|
||||
shift
|
||||
|
||||
if file_belongs_to_single_package "$file"; then
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
#
|
||||
# Remove $1 desktop file from the list of default handlers for $2 mime type
|
||||
# in $3 file dumping output to stdout.
|
||||
#
|
||||
desktop_filter_out_default_mime_handler ()
|
||||
{
|
||||
local defaults_list="$3"
|
||||
|
||||
local desktop_file="$1"
|
||||
local mime_type="$2"
|
||||
|
||||
awk -f- "$defaults_list" <<EOF
|
||||
BEGIN {
|
||||
mime_type="$mime_type"
|
||||
mime_type_regexp="~" mime_type "="
|
||||
desktop_file="$desktop_file"
|
||||
}
|
||||
\$0 ~ mime_type {
|
||||
\$0 = substr(\$0, length(mime_type) + 2);
|
||||
split(\$0, desktop_files, ";")
|
||||
remaining_desktop_files
|
||||
counter=0
|
||||
for (idx in desktop_files) {
|
||||
if (desktop_files[idx] != desktop_file) {
|
||||
++counter;
|
||||
}
|
||||
}
|
||||
if (counter) {
|
||||
printf mime_type "="
|
||||
for (idx in desktop_files) {
|
||||
if (desktop_files[idx] != desktop_file) {
|
||||
printf desktop_files[idx]
|
||||
if (--counter) {
|
||||
printf ";"
|
||||
}
|
||||
}
|
||||
}
|
||||
printf "\n"
|
||||
}
|
||||
next
|
||||
}
|
||||
|
||||
{ print }
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Remove $2 desktop file from the list of default handlers for $@ mime types
|
||||
# in $1 file.
|
||||
# Result is saved in $1 file.
|
||||
#
|
||||
desktop_uninstall_default_mime_handler_0 ()
|
||||
{
|
||||
local defaults_list=$1
|
||||
shift
|
||||
[ -f "$defaults_list" ] || return 0
|
||||
|
||||
local desktop_file="$1"
|
||||
shift
|
||||
|
||||
tmpfile1=$(mktemp)
|
||||
tmpfile2=$(mktemp)
|
||||
cat "$defaults_list" > "$tmpfile1"
|
||||
|
||||
local v
|
||||
local update=
|
||||
for mime in "$@"; do
|
||||
desktop_filter_out_default_mime_handler "$desktop_file" "$mime" "$tmpfile1" > "$tmpfile2"
|
||||
v="$tmpfile2"
|
||||
tmpfile2="$tmpfile1"
|
||||
tmpfile1="$v"
|
||||
|
||||
if ! diff -q "$tmpfile1" "$tmpfile2" > /dev/null; then
|
||||
update=yes
|
||||
desktop_trace Remove $desktop_file default handler for $mime mime type from $defaults_list file
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$update" ]; then
|
||||
cat "$tmpfile1" > "$defaults_list"
|
||||
desktop_trace "$defaults_list" file updated
|
||||
fi
|
||||
|
||||
rm -f "$tmpfile1" "$tmpfile2"
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Remove $1 desktop file from the list of default handlers for $@ mime types
|
||||
# in all known system defaults lists.
|
||||
#
|
||||
desktop_uninstall_default_mime_handler ()
|
||||
{
|
||||
for f in /usr/share/applications/defaults.list /usr/local/share/applications/defaults.list; do
|
||||
desktop_uninstall_default_mime_handler_0 "$f" "$@"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
desktop_trace ()
|
||||
{
|
||||
echo "$@"
|
||||
}
|
||||
|
||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop xdg-desktop-menu uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop
|
||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml xdg-mime uninstall /opt/sparrowwallet/lib/sparrowwallet-Sparrow-MimeInfo.xml
|
||||
do_if_file_belongs_to_single_package /opt/sparrowwallet/lib/sparrowwallet-Sparrow.desktop desktop_uninstall_default_mime_handler sparrowwallet-Sparrow.desktop application/psbt application/bitcoin-transaction application/pgp-signature x-scheme-handler/bitcoin x-scheme-handler/auth47 x-scheme-handler/lightning
|
||||
|
||||
|
||||
%clean
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.8.0</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>
|
||||
@@ -94,6 +98,21 @@
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>sparrow.icns</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.sparrowwallet.asc</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>asc</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>ASCII Armored File</string>
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>sparrow.icns</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -1,4 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!--
|
||||
This file is a copy of the jpackage WiX template (src/jdk.jpackage/windows/classes/jdk/jpackage/internal/resources/main.wxs in the JDK sources),
|
||||
customized only to add the RegistryEntries and CapabilitiesEntries components registering the auth47, bitcoin and lightning URI schemes, and their ComponentRefs in the Feature.
|
||||
When upgrading the build JDK, re-diff this file against that JDK's template and re-apply those blocks, keeping the component GUIDs unchanged.
|
||||
-->
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
|
||||
|
||||
@@ -14,6 +20,17 @@
|
||||
|
||||
<?include $(var.JpConfigDir)/overrides.wxi ?>
|
||||
|
||||
<?ifdef JpAllowUpgrades ?>
|
||||
<?define JpUpgradeVersionOnlyDetectUpgrade="no"?>
|
||||
<?else?>
|
||||
<?define JpUpgradeVersionOnlyDetectUpgrade="yes"?>
|
||||
<?endif?>
|
||||
<?ifdef JpAllowDowngrades ?>
|
||||
<?define JpUpgradeVersionOnlyDetectDowngrade="no"?>
|
||||
<?else?>
|
||||
<?define JpUpgradeVersionOnlyDetectDowngrade="yes"?>
|
||||
<?endif?>
|
||||
|
||||
<Product
|
||||
Id="$(var.JpProductCode)"
|
||||
Name="$(var.JpAppName)"
|
||||
@@ -32,45 +49,31 @@
|
||||
|
||||
<Media Id="1" Cabinet="Data.cab" EmbedCab="yes" />
|
||||
|
||||
<?ifdef JpAllowDowngrades ?>
|
||||
<?ifdef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="yes"
|
||||
Disallow="no"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
<Upgrade Id="$(var.JpProductUpgradeCode)">
|
||||
<UpgradeVersion
|
||||
OnlyDetect="$(var.JpUpgradeVersionOnlyDetectUpgrade)"
|
||||
Property="JP_UPGRADABLE_FOUND"
|
||||
Maximum="$(var.JpAppVersion)"
|
||||
MigrateFeatures="yes"
|
||||
IncludeMaximum="$(var.JpUpgradeVersionOnlyDetectUpgrade)" />
|
||||
<UpgradeVersion
|
||||
OnlyDetect="$(var.JpUpgradeVersionOnlyDetectDowngrade)"
|
||||
Property="JP_DOWNGRADABLE_FOUND"
|
||||
Minimum="$(var.JpAppVersion)"
|
||||
MigrateFeatures="yes"
|
||||
IncludeMinimum="$(var.JpUpgradeVersionOnlyDetectDowngrade)" />
|
||||
</Upgrade>
|
||||
|
||||
<?ifdef JpAllowDowngrades ?>
|
||||
<?ifndef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="yes"
|
||||
Disallow="yes"
|
||||
DisallowUpgradeErrorMessage="!(loc.DisallowUpgradeErrorMessage)"
|
||||
/>
|
||||
<CustomAction Id="JpDisallowUpgrade" Error="!(loc.DisallowUpgradeErrorMessage)" />
|
||||
<?endif?>
|
||||
<?ifndef JpAllowDowngrades ?>
|
||||
<CustomAction Id="JpDisallowDowngrade" Error="!(loc.DowngradeErrorMessage)" />
|
||||
<?endif?>
|
||||
|
||||
<?ifndef JpAllowDowngrades ?>
|
||||
<?ifdef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="no"
|
||||
Disallow="no"
|
||||
DowngradeErrorMessage="!(loc.DowngradeErrorMessage)"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
|
||||
|
||||
<?ifndef JpAllowDowngrades ?>
|
||||
<?ifndef JpAllowUpgrades ?>
|
||||
<MajorUpgrade
|
||||
AllowDowngrades="no"
|
||||
Disallow="yes"
|
||||
DowngradeErrorMessage="!(loc.DowngradeErrorMessage)"
|
||||
DisallowUpgradeErrorMessage="!(loc.DisallowUpgradeErrorMessage)"
|
||||
/>
|
||||
<?endif?>
|
||||
<?endif?>
|
||||
<CustomAction Id="JpFindRelatedProducts" BinaryKey="JpCaDll" DllEntry="FindRelatedProductsEx" />
|
||||
|
||||
<!-- Standard required root -->
|
||||
<Directory Id="TARGETDIR" Name="SourceDir"/>
|
||||
@@ -81,7 +84,7 @@
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:Auth47 Authentication URI"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
@@ -91,7 +94,7 @@
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:Bitcoin Payment URL"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
@@ -101,73 +104,89 @@
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:LNURL URI"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value="$(var.JpAppName).exe" />
|
||||
<RegistryValue Type="string" Value="[INSTALLDIR]$(var.JpAppName).exe" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[INSTALLDIR]$(var.JpAppName).exe" "%1"" />
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
</Component>
|
||||
<Component Id="CapabilitiesEntries" Guid="{BE92DD88-F799-47CC-A256-4165F9183A46}">
|
||||
<RegistryKey Root="HKMU" Key="Software\$(var.JpAppName)\Capabilities" Action="createAndRemoveOnUninstall">
|
||||
<RegistryValue Type="string" Name="ApplicationName" Value="$(var.JpAppName)"/>
|
||||
<RegistryValue Type="string" Name="ApplicationDescription" Value="$(var.JpAppDescription)"/>
|
||||
<RegistryKey Key="URLAssociations">
|
||||
<RegistryValue Type="string" Name="auth47" Value="auth47"/>
|
||||
<RegistryValue Type="string" Name="bitcoin" Value="bitcoin"/>
|
||||
<RegistryValue Type="string" Name="lightning" Value="lightning"/>
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
<RegistryValue Root="HKMU" Key="Software\RegisteredApplications" Name="$(var.JpAppName)" Type="string" Value="Software\$(var.JpAppName)\Capabilities"/>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<Feature Id="DefaultFeature" Title="!(loc.MainFeatureTitle)" Level="1">
|
||||
<ComponentGroupRef Id="Shortcuts"/>
|
||||
<ComponentGroupRef Id="Files"/>
|
||||
<ComponentGroupRef Id="FileAssociations"/>
|
||||
<ComponentGroupRef Id="FragmentOsCondition"/>
|
||||
<ComponentRef Id="RegistryEntries"/>
|
||||
<ComponentRef Id="CapabilitiesEntries"/>
|
||||
</Feature>
|
||||
|
||||
<?ifdef JpInstallDirChooser ?>
|
||||
<Binary Id="JpCaDll" SourceFile="wixhelper.dll"/>
|
||||
<CustomAction Id="JpCheckInstallDir" BinaryKey="JpCaDll" DllEntry="CheckInstallDir" />
|
||||
<CustomAction Id="JpSetARPINSTALLLOCATION" Property="ARPINSTALLLOCATION" Value="[INSTALLDIR]" />
|
||||
<CustomAction Id="JpSetARPCOMMENTS" Property="ARPCOMMENTS" Value="$(var.JpAppDescription)" />
|
||||
<CustomAction Id="JpSetARPCONTACT" Property="ARPCONTACT" Value="$(var.JpAppVendor)" />
|
||||
<CustomAction Id="JpSetARPSIZE" Property="ARPSIZE" Value="$(var.JpAppSizeKb)" />
|
||||
|
||||
<?ifdef JpHelpURL ?>
|
||||
<CustomAction Id="JpSetARPHELPLINK" Property="ARPHELPLINK" Value="$(var.JpHelpURL)" />
|
||||
<?endif?>
|
||||
|
||||
<UI>
|
||||
<?ifdef JpInstallDirChooser ?>
|
||||
<Dialog Id="JpInvalidInstallDir" Width="300" Height="85" Title="[ProductName] Setup" NoMinimize="yes">
|
||||
<Control Id="JpInvalidInstallDirYes" Type="PushButton" X="100" Y="55" Width="50" Height="15" Default="no" Cancel="no" Text="Yes">
|
||||
<Publish Event="NewDialog" Value="VerifyReadyDlg">1</Publish>
|
||||
</Control>
|
||||
<Control Id="JpInvalidInstallDirNo" Type="PushButton" X="150" Y="55" Width="50" Height="15" Default="yes" Cancel="yes" Text="No">
|
||||
<Publish Event="NewDialog" Value="InstallDirDlg">1</Publish>
|
||||
</Control>
|
||||
<Control Id="Text" Type="Text" X="25" Y="15" Width="250" Height="30" TabSkip="no">
|
||||
<Text>!(loc.message.install.dir.exist)</Text>
|
||||
</Control>
|
||||
</Dialog>
|
||||
|
||||
<!--
|
||||
Run WixUI_InstallDir dialog in the default install directory.
|
||||
-->
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR"/>
|
||||
<UIRef Id="WixUI_InstallDir" />
|
||||
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="JpCheckInstallDir" Order="3">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="JpInvalidInstallDir" Order="5">INSTALLDIR_VALID="0"</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="5">INSTALLDIR_VALID="1"</Publish>
|
||||
|
||||
<?ifndef JpLicenseRtf ?>
|
||||
<!--
|
||||
No license file provided.
|
||||
Override the dialog sequence in built-in dialog set "WixUI_InstallDir"
|
||||
to exclude license dialog.
|
||||
-->
|
||||
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="2">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2">1</Publish>
|
||||
<?endif?>
|
||||
|
||||
<?else?>
|
||||
|
||||
<?ifdef JpLicenseRtf ?>
|
||||
<UIRef Id="WixUI_Minimal" />
|
||||
<?endif?>
|
||||
|
||||
<?endif?>
|
||||
</UI>
|
||||
|
||||
<?ifdef JpLicenseRtf ?>
|
||||
<WixVariable Id="WixUILicenseRtf" Value="$(var.JpLicenseRtf)"/>
|
||||
<?ifdef JpAboutURL ?>
|
||||
<CustomAction Id="JpSetARPURLINFOABOUT" Property="ARPURLINFOABOUT" Value="$(var.JpAboutURL)" />
|
||||
<?endif?>
|
||||
|
||||
<?ifdef JpUpdateURL ?>
|
||||
<CustomAction Id="JpSetARPURLUPDATEINFO" Property="ARPURLUPDATEINFO" Value="$(var.JpUpdateURL)" />
|
||||
<?endif?>
|
||||
|
||||
<?ifdef JpIcon ?>
|
||||
<Property Id="ARPPRODUCTICON" Value="JpARPPRODUCTICON"/>
|
||||
<Icon Id="JpARPPRODUCTICON" SourceFile="$(var.JpIcon)"/>
|
||||
<?endif?>
|
||||
|
||||
<UIRef Id="JpUI"/>
|
||||
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="JpSetARPINSTALLLOCATION" After="CostFinalize">Not Installed</Custom>
|
||||
<Custom Action="JpSetARPCOMMENTS" After="CostFinalize">Not Installed</Custom>
|
||||
<Custom Action="JpSetARPCONTACT" After="CostFinalize">Not Installed</Custom>
|
||||
<Custom Action="JpSetARPSIZE" After="CostFinalize">Not Installed</Custom>
|
||||
<?ifdef JpHelpURL ?>
|
||||
<Custom Action="JpSetARPHELPLINK" After="CostFinalize">Not Installed</Custom>
|
||||
<?endif?>
|
||||
<?ifdef JpAboutURL ?>
|
||||
<Custom Action="JpSetARPURLINFOABOUT" After="CostFinalize">Not Installed</Custom>
|
||||
<?endif?>
|
||||
<?ifdef JpUpdateURL ?>
|
||||
<Custom Action="JpSetARPURLUPDATEINFO" After="CostFinalize">Not Installed</Custom>
|
||||
<?endif?>
|
||||
|
||||
<?ifndef JpAllowUpgrades ?>
|
||||
<Custom Action="JpDisallowUpgrade" After="JpFindRelatedProducts">JP_UPGRADABLE_FOUND</Custom>
|
||||
<?endif?>
|
||||
<?ifndef JpAllowDowngrades ?>
|
||||
<Custom Action="JpDisallowDowngrade" After="JpFindRelatedProducts">JP_DOWNGRADABLE_FOUND</Custom>
|
||||
<?endif?>
|
||||
<LaunchConditions Before="AppSearch"/>
|
||||
<RemoveExistingProducts Before="CostInitialize"/>
|
||||
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
|
||||
</InstallExecuteSequence>
|
||||
|
||||
<InstallUISequence>
|
||||
<Custom Action="JpFindRelatedProducts" After="FindRelatedProducts"/>
|
||||
</InstallUISequence>
|
||||
|
||||
</Product>
|
||||
</Wix>
|
||||
</Wix>
|
||||
|
||||
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,9 +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.paynym.PayNymService;
|
||||
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;
|
||||
@@ -34,6 +35,7 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
@@ -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;
|
||||
@@ -61,15 +62,20 @@ import java.awt.event.KeyEvent;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.*;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
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 {
|
||||
private static final Logger log = LoggerFactory.getLogger(AppServices.class);
|
||||
|
||||
@@ -80,23 +86,22 @@ public class AppServices {
|
||||
private static final int RATES_PERIOD_SECS = 5 * 60;
|
||||
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
|
||||
private static final int CONNECTION_DELAY_SECS = 2;
|
||||
private static final int RATES_DELAY_SECS_DEFAULT = 2;
|
||||
private static final int RATES_DELAY_SECS_WINDOWS = 5;
|
||||
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
|
||||
private static final Currency DEFAULT_FIAT_CURRENCY = Currency.getInstance("USD");
|
||||
private static final String TOR_DEFAULT_PROXY_CIRCUIT_ID = "default";
|
||||
|
||||
public static final List<Integer> TARGET_BLOCKS_RANGE = List.of(1, 2, 3, 4, 5, 10, 25, 50);
|
||||
public static final List<Long> FEE_RATES_RANGE = List.of(1L, 2L, 4L, 8L, 16L, 32L, 64L, 128L, 256L, 512L, 1024L);
|
||||
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 InteractionServices interactionServices;
|
||||
|
||||
private final SorobanServices sorobanServices = new SorobanServices();
|
||||
|
||||
private InteractionServices interactionServices;
|
||||
|
||||
private static PayNymService payNymService;
|
||||
private static HttpClientService httpClientService;
|
||||
|
||||
private final Application application;
|
||||
|
||||
@@ -104,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);
|
||||
@@ -112,6 +119,8 @@ public class AppServices {
|
||||
|
||||
private ElectrumServer.ConnectionService connectionService;
|
||||
|
||||
private ElectrumServer.FeeRatesService feeRatesService;
|
||||
|
||||
private Hwi.ScheduledEnumerateService deviceEnumerateService;
|
||||
|
||||
private VersionCheckService versionCheckService;
|
||||
@@ -124,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;
|
||||
@@ -160,6 +175,11 @@ public class AppServices {
|
||||
connectionService.cancel();
|
||||
ratesService.cancel();
|
||||
versionCheckService.cancel();
|
||||
|
||||
if(httpClientService != null) {
|
||||
HttpClientService.ShutdownService shutdownService = new HttpClientService.ShutdownService(httpClientService);
|
||||
shutdownService.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -175,20 +195,26 @@ 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() {
|
||||
Config config = Config.get();
|
||||
connectionService = createConnectionService();
|
||||
feeRatesService = createFeeRatesService();
|
||||
ratesService = createRatesService(config.getExchangeSource(), config.getFiatCurrency());
|
||||
versionCheckService = createVersionCheckService();
|
||||
torService = createTorService();
|
||||
preventSleepService = createPreventSleepService();
|
||||
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
minimumRelayFeeRate = getConfiguredMinimumRelayFeeRate(config);
|
||||
|
||||
if(config.getMode() == Mode.ONLINE) {
|
||||
if(config.requiresInternalTor()) {
|
||||
@@ -196,6 +222,8 @@ public class AppServices {
|
||||
} else {
|
||||
restartServices();
|
||||
}
|
||||
} else {
|
||||
EventManager.get().post(new DisconnectionEvent());
|
||||
}
|
||||
|
||||
addURIHandlers();
|
||||
@@ -247,13 +275,13 @@ public class AppServices {
|
||||
versionCheckService.cancel();
|
||||
}
|
||||
|
||||
if(payNymService != null) {
|
||||
PayNymService.ShutdownService shutdownService = new PayNymService.ShutdownService(payNymService);
|
||||
if(httpClientService != null) {
|
||||
HttpClientService.ShutdownService shutdownService = new HttpClientService.ShutdownService(httpClientService);
|
||||
shutdownService.start();
|
||||
}
|
||||
|
||||
if(Tor.getDefault() != null) {
|
||||
Tor.getDefault().getTorManager().destroy(true, success -> {});
|
||||
Tor.getDefault().close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,8 +307,9 @@ public class AppServices {
|
||||
onlineProperty.setValue(true);
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
|
||||
if(connectionService.getValue() != null) {
|
||||
EventManager.get().post(connectionService.getValue());
|
||||
FeeRatesUpdatedEvent event = connectionService.getValue();
|
||||
if(event != null) {
|
||||
EventManager.get().post(event);
|
||||
}
|
||||
});
|
||||
connectionService.setOnFailed(failEvent -> {
|
||||
@@ -301,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." +
|
||||
@@ -337,24 +369,38 @@ 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;
|
||||
}
|
||||
|
||||
private ElectrumServer.FeeRatesService createFeeRatesService() {
|
||||
ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService();
|
||||
feeRatesService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(feeRatesService.getValue());
|
||||
});
|
||||
|
||||
return feeRatesService;
|
||||
}
|
||||
|
||||
private ExchangeSource.RatesService createRatesService(ExchangeSource exchangeSource, Currency currency) {
|
||||
ExchangeSource.RatesService ratesService = new ExchangeSource.RatesService(
|
||||
exchangeSource == null ? DEFAULT_EXCHANGE_SOURCE : exchangeSource,
|
||||
currency == null ? DEFAULT_FIAT_CURRENCY : currency);
|
||||
//Delay startup on first run, Windows requires a longer delay
|
||||
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);
|
||||
|
||||
@@ -454,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;
|
||||
}
|
||||
@@ -469,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);
|
||||
@@ -501,30 +567,21 @@ 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;
|
||||
}
|
||||
|
||||
public static PayNymService getPayNymService() {
|
||||
if(payNymService == null) {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
payNymService = new PayNymService(torProxy);
|
||||
public static HttpClientService getHttpClientService() {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(httpClientService == null) {
|
||||
httpClientService = new HttpClientService(torProxy);
|
||||
} else {
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(!Objects.equals(payNymService.getTorProxy(), torProxy)) {
|
||||
payNymService.setTorProxy(getTorProxy());
|
||||
if(!Objects.equals(httpClientService.getTorProxy(), torProxy)) {
|
||||
httpClientService.setTorProxy(getTorProxy());
|
||||
}
|
||||
}
|
||||
|
||||
return payNymService;
|
||||
return httpClientService;
|
||||
}
|
||||
|
||||
public static HostAndPort getTorProxy() {
|
||||
@@ -557,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"));
|
||||
@@ -575,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() {
|
||||
@@ -660,17 +745,49 @@ 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 ? FALLBACK_FEE_RATE : getTargetBlockFeeRates().get(defaultTarget);
|
||||
return getTargetBlockFeeRates() == null ? getFallbackFeeRate() : getTargetBlockFeeRates().get(defaultTarget);
|
||||
}
|
||||
|
||||
public static Double getMinimumFeeRate() {
|
||||
Optional<Double> optMinFeeRate = getTargetBlockFeeRates().values().stream().min(Double::compareTo);
|
||||
Double minRate = optMinFeeRate.orElse(FALLBACK_FEE_RATE);
|
||||
Double minRate = optMinFeeRate.orElse(getFallbackFeeRate());
|
||||
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;
|
||||
}
|
||||
|
||||
public static Map<Integer, Double> getTargetBlockFeeRates() {
|
||||
return targetBlockFeeRates;
|
||||
}
|
||||
@@ -701,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;
|
||||
}
|
||||
@@ -718,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);
|
||||
}
|
||||
@@ -731,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);
|
||||
@@ -742,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);
|
||||
}
|
||||
@@ -770,13 +915,18 @@ 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Window getActiveWindow() {
|
||||
return Stage.getWindows().stream().filter(Window::isFocused).findFirst().orElse(get().walletWindows.keySet().iterator().hasNext() ? get().walletWindows.keySet().iterator().next() : null);
|
||||
return Stage.getWindows().stream().filter(Window::isFocused).findFirst().orElse(get().walletWindows.keySet().iterator().hasNext() ? get().walletWindows.keySet().iterator().next() : (Stage.getWindows().iterator().hasNext() ? Stage.getWindows().iterator().next() : null));
|
||||
}
|
||||
|
||||
public static void moveToActiveWindowScreen(Dialog<?> dialog) {
|
||||
@@ -849,6 +999,25 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
public static void openFileUriArgumentsAfterWalletLoading(Window window) {
|
||||
if(!argFiles.isEmpty() || !argUris.isEmpty()) {
|
||||
Service<Void> service = new Service<>() {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() {
|
||||
Platform.runLater(() -> openFileUriArguments(window));
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
service.setExecutor(Storage.LoadWalletService.getSingleThreadedExecutor());
|
||||
service.start();
|
||||
}
|
||||
}
|
||||
|
||||
public static void openFileUriArguments(Window window) {
|
||||
openFiles(argFiles, window);
|
||||
argFiles.clear();
|
||||
@@ -868,6 +1037,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
if(openWindow instanceof Stage) {
|
||||
((Stage)openWindow).setIconified(false);
|
||||
((Stage)openWindow).setAlwaysOnTop(true);
|
||||
((Stage)openWindow).setAlwaysOnTop(false);
|
||||
}
|
||||
@@ -875,6 +1045,8 @@ public class AppServices {
|
||||
for(File file : openFiles) {
|
||||
if(isWalletFile(file)) {
|
||||
EventManager.get().post(new RequestWalletOpenEvent(openWindow, file));
|
||||
} else if(isVerifyDownloadFile(file)) {
|
||||
EventManager.get().post(new RequestVerifyDownloadEvent(openWindow, file));
|
||||
} else {
|
||||
EventManager.get().post(new RequestTransactionOpenEvent(openWindow, file));
|
||||
}
|
||||
@@ -930,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 {
|
||||
@@ -950,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()) {
|
||||
@@ -1024,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();
|
||||
@@ -1037,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
|
||||
@@ -1062,26 +1314,36 @@ 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
|
||||
public void mempoolRateSizes(MempoolRateSizesUpdatedEvent event) {
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
if(event.getMempoolRateSizes() != null) {
|
||||
addMempoolRateSizes(event.getMempoolRateSizes());
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void feeRateSourceChanged(FeeRatesSourceChangedEvent event) {
|
||||
ElectrumServer.FeeRatesService feeRatesService = new ElectrumServer.FeeRatesService();
|
||||
feeRatesService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(feeRatesService.getValue());
|
||||
});
|
||||
//Perform once-off fee rates retrieval to immediately change displayed rates
|
||||
feeRatesService.start();
|
||||
fetchFeeRates();
|
||||
fetchBlockSummaries(Collections.emptyList());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
@@ -1222,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;
|
||||
@@ -12,7 +13,12 @@ import org.fxmisc.richtext.event.MouseOverTextEvent;
|
||||
import org.fxmisc.richtext.model.TwoDimensional;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
|
||||
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward;
|
||||
|
||||
public abstract class BaseController {
|
||||
@@ -24,14 +30,11 @@ public abstract class BaseController {
|
||||
|
||||
scriptArea.setMouseOverTextDelay(Duration.ofMillis(150));
|
||||
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
|
||||
TwoDimensional.Position position = scriptArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
|
||||
if(position.getMajor() % 2 == 0 && scriptArea.getScript().getChunks().size() > position.getMajor() / 2) {
|
||||
ScriptChunk hoverChunk = scriptArea.getScript().getChunks().get(position.getMajor()/2);
|
||||
if(!hoverChunk.isOpCode()) {
|
||||
Point2D pos = e.getScreenPosition();
|
||||
popupMsg.setText(describeScriptChunk(hoverChunk));
|
||||
popup.show(scriptArea, pos.getX(), pos.getY() + 10);
|
||||
}
|
||||
ScriptChunk hoverChunk = getScriptChunk(scriptArea, e.getCharacterIndex());
|
||||
if(hoverChunk != null) {
|
||||
Point2D pos = e.getScreenPosition();
|
||||
popupMsg.setText(describeScriptChunk(hoverChunk));
|
||||
popup.show(scriptArea, pos.getX(), pos.getY() + 10);
|
||||
}
|
||||
});
|
||||
scriptArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_END, e -> {
|
||||
@@ -52,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();
|
||||
@@ -70,14 +73,39 @@ 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();
|
||||
}
|
||||
|
||||
return "Invalid";
|
||||
}
|
||||
|
||||
public static ScriptChunk getScriptChunk(ScriptArea area, int characterIndex) {
|
||||
TwoDimensional.Position position = area.getParagraph(0).getStyleSpans().offsetToPosition(characterIndex, Backward);
|
||||
int ignoreCount = 0;
|
||||
for(int i = 0; i < position.getMajor() && i < area.getParagraph(0).getStyleSpans().getSpanCount(); i++) {
|
||||
Collection<String> styles = area.getParagraph(0).getStyleSpans().getStyleSpan(i).getStyle();
|
||||
if(i < position.getMajor() && (styles.contains("") || styles.contains("script-nest"))) {
|
||||
ignoreCount++;
|
||||
}
|
||||
}
|
||||
boolean hashScripts = List.of(P2PKH, P2SH, P2WPKH, P2WSH).stream().anyMatch(type -> type.isScriptType(area.getScript()));
|
||||
List<ScriptChunk> flatChunks = area.getScript().getChunks().stream().flatMap(chunk -> !hashScripts && chunk.isScript() ? chunk.getScript().getChunks().stream() : Stream.of(chunk)).collect(Collectors.toList());
|
||||
int chunkIndex = position.getMajor() - ignoreCount;
|
||||
if(chunkIndex < flatChunks.size()) {
|
||||
ScriptChunk chunk = flatChunks.get(chunkIndex);
|
||||
if(!chunk.isOpCode()) {
|
||||
return chunk;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
|
||||
public class BlockSummary implements Comparable<BlockSummary> {
|
||||
private final Integer height;
|
||||
private final Date timestamp;
|
||||
private final Double medianFee;
|
||||
private final Integer transactionCount;
|
||||
private final Integer weight;
|
||||
|
||||
public BlockSummary(Integer height, Date timestamp) {
|
||||
this(height, timestamp, null, null, null);
|
||||
}
|
||||
|
||||
public BlockSummary(Integer height, Date timestamp, Double medianFee, Integer transactionCount, Integer weight) {
|
||||
this.height = height;
|
||||
this.timestamp = timestamp;
|
||||
this.medianFee = medianFee;
|
||||
this.transactionCount = transactionCount;
|
||||
this.weight = weight;
|
||||
}
|
||||
|
||||
public Integer getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public Date getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public Optional<Double> getMedianFee() {
|
||||
return medianFee == null ? Optional.empty() : Optional.of(medianFee);
|
||||
}
|
||||
|
||||
public Optional<Integer> getTransactionCount() {
|
||||
return transactionCount == null ? Optional.empty() : Optional.of(transactionCount);
|
||||
}
|
||||
|
||||
public Optional<Integer> getWeight() {
|
||||
return weight == null ? Optional.empty() : Optional.of(weight);
|
||||
}
|
||||
|
||||
private static long calculateElapsedSeconds(long timestampUtc) {
|
||||
Instant timestampInstant = Instant.ofEpochMilli(timestampUtc);
|
||||
Instant nowInstant = Instant.now();
|
||||
return ChronoUnit.SECONDS.between(timestampInstant, nowInstant);
|
||||
}
|
||||
|
||||
public String getElapsed() {
|
||||
long elapsed = calculateElapsedSeconds(getTimestamp().getTime());
|
||||
if(elapsed < 0) {
|
||||
return "now";
|
||||
} else if(elapsed < 60) {
|
||||
return elapsed + "s";
|
||||
} else if(elapsed < 3600) {
|
||||
return elapsed / 60 + "m";
|
||||
} else if(elapsed < 86400) {
|
||||
return elapsed / 3600 + "h";
|
||||
} else {
|
||||
return elapsed / 86400 + "d";
|
||||
}
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return getElapsed() + ":" + getMedianFee();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(BlockSummary o) {
|
||||
return o.height - height;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.sparrow.control.KeystorePassphraseDialog;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
@@ -48,7 +49,7 @@ public class DefaultInteractionServices implements InteractionServices {
|
||||
}
|
||||
|
||||
String[] lines = content.split("\r\n|\r|\n");
|
||||
if(lines.length > 3 || org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.WINDOWS) {
|
||||
if(lines.length > 3 || OsType.getCurrent() == OsType.WINDOWS) {
|
||||
double numLines = Arrays.stream(lines).mapToDouble(line -> Math.ceil(TextUtils.computeTextWidth(Font.getDefault(), line, 0) / 300)).sum();
|
||||
alert.getDialogPane().setPrefHeight(200 + numLines * 20);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ public enum Interface {
|
||||
public static Interface get() {
|
||||
if(currentInterface == null) {
|
||||
boolean headless = java.awt.GraphicsEnvironment.isHeadless();
|
||||
boolean monocle = "Monocle".equalsIgnoreCase(System.getProperty("glass.platform"));
|
||||
boolean headlessPlatform = "Headless".equalsIgnoreCase(System.getProperty("glass.platform"));
|
||||
|
||||
if(headless || monocle) {
|
||||
if(headless || headlessPlatform) {
|
||||
currentInterface = TERMINAL;
|
||||
|
||||
if(headless && !monocle) {
|
||||
throw new UnsupportedOperationException("Headless environment detected but Monocle platform not found");
|
||||
if(headless && !headlessPlatform) {
|
||||
throw new UnsupportedOperationException("Headless environment detected but headless glass platform not found");
|
||||
}
|
||||
} else {
|
||||
currentInterface = DESKTOP;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.control.WalletIcon;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
@@ -9,13 +10,12 @@ import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.settings.SettingsGroup;
|
||||
import com.sparrowwallet.sparrow.settings.SettingsDialog;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.text.Font;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.glyphfont.GlyphFontRegistry;
|
||||
import org.controlsfx.tools.Platform;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
@@ -42,10 +42,7 @@ public class SparrowDesktop extends Application {
|
||||
public void start(Stage stage) throws Exception {
|
||||
this.mainStage = stage;
|
||||
|
||||
GlyphFontRegistry.register(new FontAwesome5());
|
||||
GlyphFontRegistry.register(new FontAwesome5Brands());
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Regular.ttf"), 13);
|
||||
Font.loadFont(AppServices.class.getResourceAsStream("/font/RobotoMono-Italic.ttf"), 11);
|
||||
initializeFonts();
|
||||
URL.setURLStreamHandlerFactory(protocol -> WalletIcon.PROTOCOL.equals(protocol) ? new WalletIcon.WalletIconStreamHandler() : null);
|
||||
|
||||
AppServices.initialize(this);
|
||||
@@ -60,8 +57,8 @@ public class SparrowDesktop extends Application {
|
||||
Config.get().setMode(mode);
|
||||
|
||||
if(mode.equals(Mode.ONLINE)) {
|
||||
PreferencesDialog preferencesDialog = new PreferencesDialog(PreferenceGroup.SERVER, true);
|
||||
Optional<Boolean> optNewWallet = preferencesDialog.showAndWait();
|
||||
SettingsDialog settingsDialog = new SettingsDialog(SettingsGroup.SERVER, true);
|
||||
Optional<Boolean> optNewWallet = settingsDialog.showAndWait();
|
||||
createNewWallet = optNewWallet.isPresent() && optNewWallet.get();
|
||||
} else if(Network.get() == Network.MAINNET) {
|
||||
Config.get().setServerType(ServerType.PUBLIC_ELECTRUM_SERVER);
|
||||
@@ -75,11 +72,8 @@ 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()));
|
||||
|
||||
if(Config.get().getAppHeight() != null && Config.get().getAppWidth() != null) {
|
||||
mainStage.setWidth(Config.get().getAppWidth());
|
||||
@@ -88,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.openFileUriArguments(stage);
|
||||
|
||||
AppServices.get().start();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.sparrowwallet.sparrow;
|
||||
|
||||
import com.beust.jcommander.JCommander;
|
||||
import com.sparrowwallet.drongo.ApplicationDir;
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
@@ -16,16 +17,26 @@ import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
public class SparrowWallet {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_ID = "sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.8.0";
|
||||
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 APP_HOME_PROPERTY = ApplicationDir.getHomeProperty(APP_NAME);
|
||||
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);
|
||||
@@ -61,12 +72,19 @@ public class SparrowWallet {
|
||||
}
|
||||
}
|
||||
|
||||
File testnetFlag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET.getName());
|
||||
Storage.logApplicationDirs();
|
||||
|
||||
File testnetFlag = new File(Storage.getConfigHome(), "network-" + Network.TESTNET.getName());
|
||||
if(testnetFlag.exists()) {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||
File testnet4Flag = new File(Storage.getConfigHome(), "network-" + Network.TESTNET4.getName());
|
||||
if(testnet4Flag.exists()) {
|
||||
Network.set(Network.TESTNET4);
|
||||
}
|
||||
|
||||
File signetFlag = new File(Storage.getConfigHome(), "network-" + Network.SIGNET.getName());
|
||||
if(signetFlag.exists()) {
|
||||
Network.set(Network.SIGNET);
|
||||
}
|
||||
@@ -79,7 +97,7 @@ public class SparrowWallet {
|
||||
|
||||
try {
|
||||
instance = new Instance(fileUriArguments);
|
||||
instance.acquireLock(); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
||||
instance.acquireLock(!fileUriArguments.isEmpty()); //If fileUriArguments is not empty, will exit app after sending fileUriArguments if lock cannot be acquired
|
||||
} catch(InstanceException e) {
|
||||
getLogger().error("Could not access application lock", e);
|
||||
}
|
||||
@@ -130,13 +148,13 @@ public class SparrowWallet {
|
||||
private final List<String> fileUriArguments;
|
||||
|
||||
public Instance(List<String> fileUriArguments) {
|
||||
super(SparrowWallet.APP_ID + "." + Network.get(), !fileUriArguments.isEmpty());
|
||||
super(SparrowWallet.APP_ID, true);
|
||||
this.fileUriArguments = fileUriArguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void receiveMessageList(List<String> messageList) {
|
||||
if(messageList != null && !messageList.isEmpty()) {
|
||||
if(messageList != null) {
|
||||
AppServices.parseFileUriArguments(messageList);
|
||||
AppServices.openFileUriArguments(null);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ public enum UnitFormat {
|
||||
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
|
||||
private final DecimalFormat tableCurrencyFormat = new DecimalFormat("0.00", getDecimalFormatSymbols());
|
||||
|
||||
public DecimalFormat getBtcFormat() {
|
||||
btcFormat.setMaximumFractionDigits(8);
|
||||
@@ -30,6 +31,10 @@ public enum UnitFormat {
|
||||
return currencyFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getTableCurrencyFormat() {
|
||||
return tableCurrencyFormat;
|
||||
}
|
||||
|
||||
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
|
||||
symbols.setDecimalSeparator('.');
|
||||
@@ -42,6 +47,7 @@ public enum UnitFormat {
|
||||
private final DecimalFormat satsFormat = new DecimalFormat("#,##0", getDecimalFormatSymbols());
|
||||
private final DecimalFormat tableBtcFormat = new DecimalFormat("0.00000000", getDecimalFormatSymbols());
|
||||
private final DecimalFormat currencyFormat = new DecimalFormat("#,##0.00", getDecimalFormatSymbols());
|
||||
private final DecimalFormat tableCurrencyFormat = new DecimalFormat("0.00", getDecimalFormatSymbols());
|
||||
|
||||
public DecimalFormat getBtcFormat() {
|
||||
btcFormat.setMaximumFractionDigits(8);
|
||||
@@ -60,6 +66,10 @@ public enum UnitFormat {
|
||||
return currencyFormat;
|
||||
}
|
||||
|
||||
public DecimalFormat getTableCurrencyFormat() {
|
||||
return tableCurrencyFormat;
|
||||
}
|
||||
|
||||
public DecimalFormatSymbols getDecimalFormatSymbols() {
|
||||
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
|
||||
symbols.setDecimalSeparator(',');
|
||||
@@ -78,6 +88,8 @@ public enum UnitFormat {
|
||||
|
||||
public abstract DecimalFormat getCurrencyFormat();
|
||||
|
||||
public abstract DecimalFormat getTableCurrencyFormat();
|
||||
|
||||
public String formatBtcValue(Long amount) {
|
||||
return getBtcFormat().format(amount.doubleValue() / Transaction.SATOSHIS_PER_BITCOIN);
|
||||
}
|
||||
@@ -94,6 +106,10 @@ public enum UnitFormat {
|
||||
return getCurrencyFormat().format(amount);
|
||||
}
|
||||
|
||||
public String tableFormatCurrencyValue(double amount) {
|
||||
return getTableCurrencyFormat().format(amount);
|
||||
}
|
||||
|
||||
public String getGroupingSeparator() {
|
||||
return Character.toString(getDecimalFormatSymbols().getGroupingSeparator());
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ public class WelcomeDialog extends Dialog<Mode> {
|
||||
welcomeController.initializeView();
|
||||
|
||||
dialogPane.setPrefWidth(600);
|
||||
dialogPane.setPrefHeight(520);
|
||||
dialogPane.setPrefHeight(540);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("welcome.css").toExternalForm());
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
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.*;
|
||||
|
||||
public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
private static final int MAX_SHOWN_ACCOUNTS = 8;
|
||||
|
||||
private final ComboBox<StandardAccount> standardAccountCombo;
|
||||
private boolean discoverAccounts = false;
|
||||
|
||||
@@ -44,32 +43,33 @@ 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()));
|
||||
}
|
||||
}
|
||||
|
||||
List<StandardAccount> availableAccounts = new ArrayList<>();
|
||||
for(StandardAccount standardAccount : StandardAccount.values()) {
|
||||
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
|
||||
if(!existingIndexes.contains(standardAccount.getAccountNumber()) && !StandardAccount.isWhirlpoolAccount(standardAccount) && availableAccounts.size() <= MAX_SHOWN_ACCOUNTS) {
|
||||
availableAccounts.add(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);
|
||||
}
|
||||
}
|
||||
@@ -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,19 +58,25 @@ 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());
|
||||
tooltip.showConfirmations(transactionEntry.confirmationsProperty(), transactionEntry.isCoinbase());
|
||||
|
||||
if(transactionEntry.isConfirming()) {
|
||||
ConfirmationProgressIndicator arc = new ConfirmationProgressIndicator(transactionEntry.getConfirmations());
|
||||
@@ -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);
|
||||
@@ -119,6 +127,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
private static final class CoinTooltip extends Tooltip {
|
||||
private final IntegerProperty confirmationsProperty = new SimpleIntegerProperty();
|
||||
private boolean showConfirmations;
|
||||
private boolean isCoinbase;
|
||||
private String value;
|
||||
|
||||
public void setValue(String value) {
|
||||
@@ -126,8 +135,9 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
setTooltipText();
|
||||
}
|
||||
|
||||
public void showConfirmations(IntegerProperty txEntryConfirmationsProperty) {
|
||||
public void showConfirmations(IntegerProperty txEntryConfirmationsProperty, boolean coinbase) {
|
||||
showConfirmations = true;
|
||||
isCoinbase = coinbase;
|
||||
|
||||
int confirmations = txEntryConfirmationsProperty.get();
|
||||
if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||
@@ -146,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() + ")" : ""));
|
||||
}
|
||||
@@ -155,7 +173,7 @@ class CoinCell extends TreeTableCell<Entry, Number> implements ConfirmationsList
|
||||
if(confirmations == 0) {
|
||||
return "Unconfirmed in mempool";
|
||||
} else if(confirmations < BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM) {
|
||||
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s");
|
||||
return confirmations + " confirmation" + (confirmations == 1 ? "" : "s") + (isCoinbase ? ", immature coinbase" : "");
|
||||
} else {
|
||||
return BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM + "+ confirmations";
|
||||
}
|
||||
|
||||
@@ -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,12 +10,15 @@ import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.MouseButton;
|
||||
|
||||
public class CopyableCoinLabel extends CopyableLabel {
|
||||
private final LongProperty valueProperty = new SimpleLongProperty(-1);
|
||||
private final Tooltip tooltip;
|
||||
private final CoinContextMenu contextMenu;
|
||||
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
|
||||
public CopyableCoinLabel() {
|
||||
this("Unknown");
|
||||
}
|
||||
@@ -23,6 +26,25 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
public CopyableCoinLabel(String text) {
|
||||
super(text);
|
||||
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();
|
||||
}
|
||||
|
||||
if(bitcoinUnit == BitcoinUnit.SATOSHIS) {
|
||||
bitcoinUnit = BitcoinUnit.BTC;
|
||||
} else {
|
||||
bitcoinUnit = BitcoinUnit.SATOSHIS;
|
||||
}
|
||||
|
||||
refresh(Config.get().getUnitFormat(), bitcoinUnit);
|
||||
});
|
||||
|
||||
tooltip = new Tooltip();
|
||||
contextMenu = new CoinContextMenu();
|
||||
}
|
||||
@@ -48,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);
|
||||
|
||||
@@ -63,6 +92,8 @@ public class CopyableCoinLabel extends CopyableLabel {
|
||||
unit = (value >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS);
|
||||
}
|
||||
|
||||
this.bitcoinUnit = unit;
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
tooltip.setText(satsValue);
|
||||
setText(btcValue);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ public abstract class DeviceDialog<R> extends Dialog<R> {
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(360);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == cancelButtonType ? null : getResult());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,778 @@
|
||||
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;
|
||||
import com.sparrowwallet.drongo.pgp.PGPVerificationException;
|
||||
import com.sparrowwallet.drongo.pgp.PGPVerificationResult;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.net.VersionCheckService;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.Dragboard;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppController.DRAG_OVER_CLASS;
|
||||
|
||||
public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private static final Logger log = LoggerFactory.getLogger(DownloadVerifierDialog.class);
|
||||
|
||||
private static final DateFormat signatureDateFormat = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy z");
|
||||
|
||||
private static final long MAX_VALID_MANIFEST_SIZE = 100 * 1024;
|
||||
private static final String SHA256SUMS_MANIFEST_PREFIX = "sha256sums";
|
||||
|
||||
private static final List<String> SIGNATURE_EXTENSIONS = List.of("asc", "sig", "gpg");
|
||||
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", "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_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;
|
||||
|
||||
private final ObjectProperty<File> signature = new SimpleObjectProperty<>();
|
||||
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();
|
||||
|
||||
private final Label signedBy;
|
||||
private final Label releaseHash;
|
||||
private final Label releaseVerified;
|
||||
private final Hyperlink releaseLink;
|
||||
|
||||
private static File lastFileParent;
|
||||
|
||||
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());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeader(new Header());
|
||||
setupDrag(dialogPane);
|
||||
|
||||
VBox vBox = new VBox();
|
||||
vBox.setSpacing(20);
|
||||
vBox.setPadding(new Insets(20, 10, 10, 20));
|
||||
|
||||
Form form = new Form();
|
||||
Fieldset filesFieldset = new Fieldset();
|
||||
filesFieldset.setText("Files");
|
||||
filesFieldset.setSpacing(10);
|
||||
|
||||
String version = VersionCheckService.getVersion() != null ? VersionCheckService.getVersion() : "x.x.x";
|
||||
|
||||
Field signatureField = setupField(signature, "Signature", SIGNATURE_EXTENSIONS, false, "sparrow-" + version + "-manifest.txt", null);
|
||||
Field manifestField = setupField(manifest, "Manifest", MANIFEST_EXTENSIONS, false, "sparrow-" + version + "-manifest", manifestDisabled);
|
||||
Field publicKeyField = setupField(publicKey, "Public Key", PUBLIC_KEY_EXTENSIONS, true, "pgp_keys", publicKeyDisabled);
|
||||
Field releaseFileField = setupField(release, "Release File", getReleaseFileExtensions(), false, getReleaseFileExample(version), null);
|
||||
|
||||
filesFieldset.getChildren().addAll(signatureField, manifestField, publicKeyField, releaseFileField);
|
||||
form.getChildren().add(filesFieldset);
|
||||
|
||||
Fieldset resultsFieldset = new Fieldset();
|
||||
resultsFieldset.setText("Results");
|
||||
resultsFieldset.setSpacing(10);
|
||||
|
||||
signedBy = new Label();
|
||||
Field signedByField = setupResultField(signedBy, "Signed By");
|
||||
|
||||
releaseHash = new Label();
|
||||
Field hashMatchedField = setupResultField(releaseHash, "Release Hash");
|
||||
|
||||
releaseVerified = new Label();
|
||||
Field releaseVerifiedField = setupResultField(releaseVerified, "Verified");
|
||||
|
||||
releaseLink = new Hyperlink("");
|
||||
releaseVerifiedField.getInputs().add(releaseLink);
|
||||
releaseLink.setOnAction(event -> {
|
||||
if(release.get() != null && release.get().exists()) {
|
||||
if(release.get().getName().toLowerCase(Locale.ROOT).startsWith("sparrow")) {
|
||||
Optional<ButtonType> optType = AppServices.showAlertDialog("Exit Sparrow?", "Sparrow must be closed before installation. Exit?", Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.YES) {
|
||||
javafx.application.Platform.exit();
|
||||
AppServices.get().getApplication().getHostServices().showDocument("file://" + release.get().getAbsolutePath());
|
||||
}
|
||||
} else {
|
||||
AppServices.get().getApplication().getHostServices().showDocument("file://" + release.get().getAbsolutePath());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
resultsFieldset.getChildren().addAll(signedByField, hashMatchedField, releaseVerifiedField);
|
||||
form.getChildren().add(resultsFieldset);
|
||||
|
||||
vBox.getChildren().addAll(form);
|
||||
dialogPane.setContent(vBox);
|
||||
|
||||
ButtonType clearButtonType = new javafx.scene.control.ButtonType("Clear", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
ButtonType closeButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.OK_DONE);
|
||||
dialogPane.getButtonTypes().addAll(clearButtonType, closeButtonType);
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
if(ButtonBar.ButtonData.CANCEL_CLOSE.equals(getResult())) {
|
||||
signature.set(null);
|
||||
manifest.set(null);
|
||||
publicKey.set(null);
|
||||
release.set(null);
|
||||
signedBy.setText("");
|
||||
signedBy.setGraphic(null);
|
||||
signedBy.setTooltip(null);
|
||||
releaseHash.setText("");
|
||||
releaseHash.setGraphic(null);
|
||||
releaseVerified.setText("");
|
||||
releaseVerified.setGraphic(null);
|
||||
releaseLink.setText("");
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
setResultConverter(ButtonType::getButtonData);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
dialogPane.setPrefWidth(900);
|
||||
setResizable(true);
|
||||
|
||||
signature.addListener((observable, oldValue, signatureFile) -> {
|
||||
if(signatureFile != null) {
|
||||
boolean verify = true;
|
||||
File actualSignatureFile = findSignatureFile(signatureFile);
|
||||
if(actualSignatureFile != null && !actualSignatureFile.equals(signature.get())) {
|
||||
signature.set(actualSignatureFile);
|
||||
verify = false;
|
||||
} else if(PGPUtils.signatureContainsManifest(signatureFile)) {
|
||||
manifest.set(signatureFile);
|
||||
verify = false;
|
||||
} else {
|
||||
File manifestFile = findManifestFile(signatureFile);
|
||||
if(manifestFile != null && !manifestFile.equals(manifest.get())) {
|
||||
manifest.set(manifestFile);
|
||||
verify = false;
|
||||
}
|
||||
}
|
||||
|
||||
if(verify) {
|
||||
verify();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
manifest.addListener((observable, oldValue, manifestFile) -> {
|
||||
if(manifestFile != null) {
|
||||
boolean verify = true;
|
||||
try {
|
||||
Map<File, String> manifestMap = getManifest(manifestFile);
|
||||
File releaseFile = findReleaseFile(manifestFile, manifestMap);
|
||||
if(releaseFile != null && !releaseFile.equals(release.get())) {
|
||||
release.set(releaseFile);
|
||||
verify = false;
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.debug("Error reading manifest file", e);
|
||||
verify = false;
|
||||
} catch(InvalidManifestException e) {
|
||||
release.set(manifestFile);
|
||||
verify = false;
|
||||
}
|
||||
|
||||
if(verify) {
|
||||
verify();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
publicKey.addListener((observable, oldValue, newValue) -> {
|
||||
verify();
|
||||
});
|
||||
|
||||
release.addListener((observable, oldValue, releaseFile) -> {
|
||||
if(releaseFile != null) {
|
||||
initial.set(null);
|
||||
}
|
||||
verify();
|
||||
});
|
||||
|
||||
if(initialFile != null) {
|
||||
javafx.application.Platform.runLater(() -> {
|
||||
initial.set(initialFile);
|
||||
signature.set(initialFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void setupDrag(DialogPane dialogPane) {
|
||||
dialogPane.setOnDragOver(event -> {
|
||||
if(event.getGestureSource() != dialogPane && event.getDragboard().hasFiles()) {
|
||||
event.acceptTransferModes(TransferMode.LINK);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
dialogPane.setOnDragDropped(event -> {
|
||||
Dragboard db = event.getDragboard();
|
||||
boolean success = false;
|
||||
if(db.hasFiles()) {
|
||||
for(File file : db.getFiles()) {
|
||||
if(isVerifyDownloadFile(file)) {
|
||||
signature.set(file);
|
||||
break;
|
||||
}
|
||||
}
|
||||
success = true;
|
||||
}
|
||||
event.setDropCompleted(success);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
dialogPane.setOnDragEntered(event -> {
|
||||
dialogPane.getStyleClass().add(DRAG_OVER_CLASS);
|
||||
});
|
||||
|
||||
dialogPane.setOnDragExited(event -> {
|
||||
dialogPane.getStyleClass().removeAll(DRAG_OVER_CLASS);
|
||||
});
|
||||
}
|
||||
|
||||
private void verify() {
|
||||
manifestDisabled.set(false);
|
||||
publicKeyDisabled.set(false);
|
||||
|
||||
if(signature.get() == null || manifest.get() == null) {
|
||||
clearReleaseFields();
|
||||
return;
|
||||
}
|
||||
|
||||
PGPVerifyService pgpVerifyService = new PGPVerifyService(signature.get(), manifest.get(), publicKey.get());
|
||||
pgpVerifyService.setOnRunning(event -> {
|
||||
signedBy.setText("Verifying...");
|
||||
signedBy.setGraphic(GlyphUtils.getBusyGlyph());
|
||||
signedBy.setTooltip(null);
|
||||
clearReleaseFields();
|
||||
});
|
||||
pgpVerifyService.setOnSucceeded(event -> {
|
||||
PGPVerificationResult result = pgpVerifyService.getValue();
|
||||
|
||||
String message = result.userId() + " on " + signatureDateFormat.format(result.signatureTimestamp()) + (result.expired() ? " (key expired)" : "");
|
||||
signedBy.setText(message);
|
||||
signedBy.setGraphic(result.expired() ? GlyphUtils.getWarningGlyph() : GlyphUtils.getSuccessGlyph());
|
||||
signedBy.setTooltip(new Tooltip(result.fingerprint()));
|
||||
|
||||
if(!result.expired() && result.keySource() != PGPKeySource.USER) {
|
||||
publicKeyDisabled.set(true);
|
||||
}
|
||||
|
||||
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());
|
||||
releaseHash.setTooltip(null);
|
||||
releaseVerified.setText("Ready to install ");
|
||||
releaseVerified.setGraphic(GlyphUtils.getSuccessGlyph());
|
||||
releaseLink.setText(release.get().getName());
|
||||
} else {
|
||||
verifyManifest();
|
||||
}
|
||||
});
|
||||
pgpVerifyService.setOnFailed(event -> {
|
||||
Throwable e = event.getSource().getException();
|
||||
signedBy.setText(getDisplayMessage(e));
|
||||
signedBy.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
signedBy.setTooltip(null);
|
||||
clearReleaseFields();
|
||||
});
|
||||
|
||||
pgpVerifyService.start();
|
||||
}
|
||||
|
||||
private void clearReleaseFields() {
|
||||
releaseHash.setText("");
|
||||
releaseHash.setGraphic(null);
|
||||
releaseHash.setTooltip(null);
|
||||
releaseVerified.setText("");
|
||||
releaseVerified.setGraphic(null);
|
||||
releaseLink.setText("");
|
||||
}
|
||||
|
||||
private void verifyManifest() {
|
||||
File releaseFile = release.get();
|
||||
if(releaseFile != null && releaseFile.exists()) {
|
||||
FileSha256Service hashService = new FileSha256Service(releaseFile);
|
||||
hashService.setOnRunning(event -> {
|
||||
releaseHash.setText("Calculating...");
|
||||
releaseHash.setGraphic(GlyphUtils.getBusyGlyph());
|
||||
releaseHash.setTooltip(null);
|
||||
releaseVerified.setText("");
|
||||
releaseVerified.setGraphic(null);
|
||||
releaseLink.setText("");
|
||||
});
|
||||
hashService.setOnSucceeded(event -> {
|
||||
String calculatedHash = hashService.getValue();
|
||||
try {
|
||||
Map<File, String> manifestMap = getManifest(manifest.get());
|
||||
String manifestHash = getManifestHash(releaseFile.getName(), manifestMap);
|
||||
if(calculatedHash.equalsIgnoreCase(manifestHash)) {
|
||||
releaseHash.setText("Matched manifest hash");
|
||||
releaseHash.setGraphic(GlyphUtils.getSuccessGlyph());
|
||||
releaseHash.setTooltip(new Tooltip(calculatedHash));
|
||||
releaseVerified.setText("Ready to install ");
|
||||
releaseVerified.setGraphic(GlyphUtils.getSuccessGlyph());
|
||||
releaseLink.setText(releaseFile.getName());
|
||||
} else if(manifestHash == null) {
|
||||
releaseHash.setText("Could not find manifest hash for " + releaseFile.getName());
|
||||
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseHash.setTooltip(new Tooltip("Manifest hashes provided for:\n" + manifestMap.keySet().stream().map(File::getName).collect(Collectors.joining("\n"))));
|
||||
releaseVerified.setText("Cannot verify " + releaseFile.getName());
|
||||
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseLink.setText("");
|
||||
} else {
|
||||
releaseHash.setText("Did not match manifest hash");
|
||||
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseHash.setTooltip(new Tooltip("Calculated Hash: " + calculatedHash + "\nManifest Hash: " + manifestHash));
|
||||
releaseVerified.setText("Cannot verify " + releaseFile.getName());
|
||||
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseLink.setText("");
|
||||
}
|
||||
} catch(IOException | InvalidManifestException e) {
|
||||
releaseHash.setText("Could not read manifest");
|
||||
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseHash.setTooltip(new Tooltip(e.getMessage()));
|
||||
releaseVerified.setText("Cannot verify " + releaseFile.getName());
|
||||
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseLink.setText("");
|
||||
}
|
||||
});
|
||||
hashService.setOnFailed(event -> {
|
||||
releaseHash.setText("Could not calculate manifest");
|
||||
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseHash.setTooltip(new Tooltip(event.getSource().getException().getMessage()));
|
||||
releaseVerified.setText("Cannot verify " + releaseFile.getName());
|
||||
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseLink.setText("");
|
||||
});
|
||||
hashService.start();
|
||||
} else {
|
||||
releaseHash.setText("No release file");
|
||||
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseHash.setTooltip(null);
|
||||
releaseVerified.setText("Not verified");
|
||||
releaseVerified.setGraphic(GlyphUtils.getFailureGlyph());
|
||||
releaseLink.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
private Field setupField(ObjectProperty<File> fileProperty, String title, List<String> extensions, boolean optional, String example, BooleanProperty disabledProperty) {
|
||||
Field field = new Field();
|
||||
field.setText(title + ":");
|
||||
FileField fileField = new FileField(fileProperty, title, extensions, optional, example, disabledProperty);
|
||||
field.getInputs().add(fileField);
|
||||
return field;
|
||||
}
|
||||
|
||||
private Field setupResultField(Label label, String title) {
|
||||
Field field = new Field();
|
||||
field.setText(title + ":");
|
||||
field.getInputs().add(label);
|
||||
label.setGraphicTextGap(8);
|
||||
return field;
|
||||
}
|
||||
|
||||
public static Map<File, String> getManifest(File manifest) throws IOException, InvalidManifestException {
|
||||
if(manifest.length() > MAX_VALID_MANIFEST_SIZE) {
|
||||
throw new InvalidManifestException();
|
||||
}
|
||||
|
||||
try(InputStream manifestStream = new FileInputStream(manifest)) {
|
||||
return getManifest(manifestStream);
|
||||
}
|
||||
}
|
||||
|
||||
public static Map<File, String> getManifest(InputStream manifestStream) throws IOException {
|
||||
Map<File, String> manifest = new HashMap<>();
|
||||
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(manifestStream, StandardCharsets.UTF_8));
|
||||
String line;
|
||||
while((line = reader.readLine()) != null) {
|
||||
String[] parts = line.split("\\s+");
|
||||
if(parts.length > 1 && parts[0].length() == 64) {
|
||||
String manifestHash = parts[0];
|
||||
String manifestFileName = parts[1];
|
||||
if(manifestFileName.startsWith("*") || manifestFileName.startsWith("U") || manifestFileName.startsWith("^")) {
|
||||
manifestFileName = manifestFileName.substring(1);
|
||||
}
|
||||
manifest.put(new File(manifestFileName), manifestHash);
|
||||
}
|
||||
}
|
||||
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private String getManifestHash(String contentFileName, Map<File, String> manifest) {
|
||||
for(Map.Entry<File, String> entry : manifest.entrySet()) {
|
||||
if(contentFileName.equalsIgnoreCase(entry.getKey().getName())) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private File findSignatureFile(File providedFile) {
|
||||
for(String extension : SIGNATURE_EXTENSIONS) {
|
||||
File signatureFile = new File(providedFile.getParentFile(), providedFile.getName() + "." + extension);
|
||||
if(signatureFile.exists()) {
|
||||
return signatureFile;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
File signatureFile = new File(providedFile.getParentFile(), SPARROW_RELEASE_PREFIX + version + SPARROW_SIGNATURE_SUFFIX);
|
||||
if(signatureFile.exists()) {
|
||||
return signatureFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private File findManifestFile(File providedFile) {
|
||||
String signatureName = providedFile.getName();
|
||||
if(signatureName.length() > 4 && SIGNATURE_EXTENSIONS.stream().anyMatch(ext -> signatureName.toLowerCase(Locale.ROOT).endsWith("." + ext))) {
|
||||
File manifestFile = new File(providedFile.getParent(), signatureName.substring(0, signatureName.length() - 4));
|
||||
if(manifestFile.exists()) {
|
||||
return manifestFile;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
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(""));
|
||||
|
||||
for(List<String> extensions : extensionLists) {
|
||||
for(File file : manifestMap.keySet()) {
|
||||
if(extensions.stream().anyMatch(ext -> file.getName().toLowerCase(Locale.ROOT).endsWith(ext))) {
|
||||
File releaseFile = new File(manifestFile.getParent(), file.getName());
|
||||
if(releaseFile.exists()) {
|
||||
return releaseFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<String> getReleaseFileExtensions() {
|
||||
OsType osType = OsType.getCurrent();
|
||||
switch(osType) {
|
||||
case MACOS -> {
|
||||
return MACOS_RELEASE_EXTENSIONS;
|
||||
}
|
||||
case WINDOWS -> {
|
||||
return WINDOWS_RELEASE_EXTENSIONS;
|
||||
}
|
||||
default -> {
|
||||
return LINUX_RELEASE_EXTENSIONS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getReleaseFileExample(String version) {
|
||||
OsType osType = OsType.getCurrent();
|
||||
String arch = System.getProperty("os.arch");
|
||||
switch(osType) {
|
||||
case MACOS -> {
|
||||
return "Sparrow-" + version + "-" + arch;
|
||||
}
|
||||
case WINDOWS -> {
|
||||
return "Sparrow-" + version;
|
||||
}
|
||||
default -> {
|
||||
return "sparrow_" + version + "-1_" + (arch.equals("aarch64") ? "arm64" : arch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getDisplayMessage(Throwable e) {
|
||||
String message = e.getMessage();
|
||||
message = message.substring(0, 1).toUpperCase(Locale.ROOT) + message.substring(1);
|
||||
|
||||
if(message.endsWith(".")) {
|
||||
message = message.substring(0, message.length() - 1);
|
||||
}
|
||||
|
||||
if(message.equals("Invalid header encountered")) {
|
||||
message += ", not a valid signature file";
|
||||
}
|
||||
|
||||
if(message.startsWith("Malformed message")) {
|
||||
message = "Not a valid signature file";
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public static boolean isVerifyDownloadFile(File file) {
|
||||
if(file != null) {
|
||||
String name = file.getName().toLowerCase(Locale.ROOT);
|
||||
if(name.length() > 4 && SIGNATURE_EXTENSIONS.stream().anyMatch(ext -> name.endsWith("." + ext))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(MANIFEST_EXTENSIONS.stream().anyMatch(ext -> name.endsWith("." + ext)) || name.startsWith(SHA256SUMS_MANIFEST_PREFIX)) {
|
||||
try {
|
||||
Map<File, String> manifest = getManifest(file);
|
||||
return !manifest.isEmpty();
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
getStyleClass().add("header-panel");
|
||||
|
||||
VBox vBox = new VBox();
|
||||
vBox.setPadding(new Insets(10, 0, 0, 0));
|
||||
|
||||
Label headerLabel = new Label("Verify Download");
|
||||
headerLabel.setWrapText(true);
|
||||
headerLabel.setAlignment(Pos.CENTER_LEFT);
|
||||
headerLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
headerLabel.setMaxHeight(Double.MAX_VALUE);
|
||||
|
||||
CopyableLabel descriptionLabel = new CopyableLabel("Download the release file, GPG signature and optional manifest of a project to verify the download integrity");
|
||||
descriptionLabel.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
vBox.getChildren().addAll(headerLabel, descriptionLabel);
|
||||
add(vBox, 0, 0);
|
||||
|
||||
StackPane graphicContainer = new DialogImage(DialogImage.Type.SPARROW);
|
||||
graphicContainer.getStyleClass().add("graphic-container");
|
||||
add(graphicContainer, 1, 0);
|
||||
|
||||
ColumnConstraints textColumn = new ColumnConstraints();
|
||||
textColumn.setFillWidth(true);
|
||||
textColumn.setHgrow(Priority.ALWAYS);
|
||||
ColumnConstraints graphicColumn = new ColumnConstraints();
|
||||
graphicColumn.setFillWidth(false);
|
||||
graphicColumn.setHgrow(Priority.NEVER);
|
||||
getColumnConstraints().setAll(textColumn , graphicColumn);
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileField extends HBox {
|
||||
private final ObjectProperty<File> fileProperty;
|
||||
|
||||
public FileField(ObjectProperty<File> fileProperty, String title, List<String> extensions, boolean optional, String example, BooleanProperty disabledProperty) {
|
||||
super(10);
|
||||
this.fileProperty = fileProperty;
|
||||
TextField textField = new TextField();
|
||||
textField.setEditable(false);
|
||||
textField.setPromptText("e.g. " + example + formatExtensionsList(extensions) + (optional ? " (optional)" : ""));
|
||||
textField.setOnMouseClicked(event -> browseForFile(title, extensions));
|
||||
Button browseButton = new Button("Browse...");
|
||||
browseButton.setOnAction(event -> browseForFile(title, extensions));
|
||||
getChildren().addAll(textField, browseButton);
|
||||
HBox.setHgrow(textField, Priority.ALWAYS);
|
||||
|
||||
fileProperty.addListener((observable, oldValue, file) -> {
|
||||
textField.setText(file == null ? "" : file.getAbsolutePath());
|
||||
if(file != null) {
|
||||
lastFileParent = file.getParentFile();
|
||||
}
|
||||
});
|
||||
|
||||
if(disabledProperty != null) {
|
||||
disabledProperty.addListener((observable, oldValue, disabled) -> {
|
||||
textField.setDisable(disabled);
|
||||
browseButton.setDisable(disabled);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void browseForFile(String title, List<String> extensions) {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open File");
|
||||
File userDir = new File(System.getProperty("user.home"));
|
||||
File downloadsDir = new File(userDir, "Downloads");
|
||||
fileChooser.setInitialDirectory(lastFileParent != null ? lastFileParent : (downloadsDir.exists() ? downloadsDir : userDir));
|
||||
fileChooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter(title + " files", extensions));
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
if(file != null) {
|
||||
fileProperty.set(file);
|
||||
}
|
||||
}
|
||||
|
||||
public String formatExtensionsList(List<String> items) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for(int i = 0; i < items.size(); i++) {
|
||||
result.append(".").append(items.get(i));
|
||||
|
||||
if (i < items.size() - 1) {
|
||||
result.append(", ");
|
||||
}
|
||||
|
||||
if (i == items.size() - 2) {
|
||||
result.append("or ");
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
}
|
||||
|
||||
private static class PGPVerifyService extends Service<PGPVerificationResult> {
|
||||
private final File signature;
|
||||
private final File manifest;
|
||||
private final File publicKey;
|
||||
|
||||
public PGPVerifyService(File signature, File manifest, File publicKey) {
|
||||
this.signature = signature;
|
||||
this.manifest = manifest;
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<PGPVerificationResult> createTask() {
|
||||
return new Task<>() {
|
||||
protected PGPVerificationResult call() throws IOException, PGPVerificationException {
|
||||
boolean detachedSignature = !manifest.equals(signature);
|
||||
|
||||
try(InputStream publicKeyStream = publicKey == null ? null : new FileInputStream(publicKey);
|
||||
InputStream contentStream = new BufferedInputStream(new FileInputStream(manifest));
|
||||
InputStream detachedSignatureStream = detachedSignature ? new FileInputStream(signature) : null) {
|
||||
return PGPUtils.verify(publicKeyStream, contentStream, detachedSignatureStream);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileSha256Service extends Service<String> {
|
||||
private final File file;
|
||||
|
||||
public FileSha256Service(File file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<String> createTask() {
|
||||
return new Task<>() {
|
||||
protected String call() throws IOException {
|
||||
try(InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
|
||||
return sha256(inputStream);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private String sha256(InputStream stream) throws IOException {
|
||||
try {
|
||||
final byte[] buffer = new byte[1024 * 1024];
|
||||
final MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||
int bytesRead = 0;
|
||||
while((bytesRead = stream.read(buffer)) >= 0) {
|
||||
if (bytesRead > 0) {
|
||||
sha256.update(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
return Utils.bytesToHex(sha256.digest());
|
||||
} catch(NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class InvalidManifestException extends Exception { }
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -352,19 +361,21 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
List<BlockTransactionHashIndex> ourOutputs = transactionEntry.getChildren().stream()
|
||||
.filter(e -> e instanceof HashIndexEntry)
|
||||
.map(e -> (HashIndexEntry)e)
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT))
|
||||
.filter(e -> e.getType().equals(HashIndexEntry.Type.OUTPUT) && e.isSpendable())
|
||||
.map(HashIndexEntry::getHashIndex)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if(ourOutputs.isEmpty()) {
|
||||
throw new IllegalStateException("Cannot create CPFP without any wallet outputs to spend");
|
||||
AppServices.showErrorDialog("No spendable outputs", "None of the outputs on this transaction are spendable.\n\nEnsure that the outputs are not frozen" +
|
||||
(transactionEntry.getConfirmations() <= 0 ? ", and spending unconfirmed UTXOs is allowed." : "."));
|
||||
return;
|
||||
}
|
||||
|
||||
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()));
|
||||
@@ -388,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) {
|
||||
@@ -457,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) {
|
||||
@@ -473,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;
|
||||
@@ -541,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());
|
||||
@@ -550,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 -> {
|
||||
@@ -561,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 -> {
|
||||
@@ -583,12 +599,14 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
getItems().add(createCpfp);
|
||||
}
|
||||
|
||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||
openBlockExplorer.setOnAction(AE -> {
|
||||
hide();
|
||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||
});
|
||||
getItems().add(openBlockExplorer);
|
||||
if(!Config.get().isBlockExplorerDisabled()) {
|
||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||
openBlockExplorer.setOnAction(AE -> {
|
||||
hide();
|
||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||
});
|
||||
getItems().add(openBlockExplorer);
|
||||
}
|
||||
|
||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||
copyTxid.setOnAction(AE -> {
|
||||
@@ -602,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());
|
||||
@@ -610,12 +628,16 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
hide();
|
||||
EventManager.get().post(new ViewTransactionEvent(this.getOwnerWindow(), blockTransaction));
|
||||
});
|
||||
getItems().add(viewTransaction);
|
||||
|
||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||
openBlockExplorer.setOnAction(AE -> {
|
||||
hide();
|
||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||
});
|
||||
if(!Config.get().isBlockExplorerDisabled()) {
|
||||
MenuItem openBlockExplorer = new MenuItem("Open in Block Explorer");
|
||||
openBlockExplorer.setOnAction(AE -> {
|
||||
hide();
|
||||
AppServices.openBlockExplorer(blockTransaction.getHashAsString());
|
||||
});
|
||||
getItems().add(openBlockExplorer);
|
||||
}
|
||||
|
||||
MenuItem copyDate = new MenuItem("Copy Date");
|
||||
copyDate.setOnAction(AE -> {
|
||||
@@ -624,6 +646,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
content.putString(date);
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyDate);
|
||||
|
||||
MenuItem copyTxid = new MenuItem("Copy Transaction ID");
|
||||
copyTxid.setOnAction(AE -> {
|
||||
@@ -632,6 +655,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
content.putString(blockTransaction.getHashAsString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
getItems().add(copyTxid);
|
||||
|
||||
MenuItem copyHeight = new MenuItem("Copy Block Height");
|
||||
copyHeight.setOnAction(AE -> {
|
||||
@@ -640,14 +664,13 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
content.putString(blockTransaction.getHeight() > 0 ? Integer.toString(blockTransaction.getHeight()) : "Mempool");
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
|
||||
getItems().addAll(viewTransaction, openBlockExplorer, copyDate, copyTxid, copyHeight);
|
||||
getItems().add(copyHeight);
|
||||
}
|
||||
}
|
||||
|
||||
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 -> {
|
||||
@@ -796,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");
|
||||
@@ -816,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,24 +8,34 @@ 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);
|
||||
setShowTickLabels(true);
|
||||
setShowTickMarks(true);
|
||||
setBlockIncrement(Math.log(1.02) / Math.log(2));
|
||||
|
||||
setLabelFormatter(new StringConverter<Double>() {
|
||||
setLabelFormatter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Double object) {
|
||||
return Long.toString(FEE_RATES_RANGE.get(object.intValue()));
|
||||
Double feeRate = AppServices.getLongFeeRatesRange().get(object.intValue());
|
||||
if(isLongFeeRange() && feeRate >= 1000) {
|
||||
return INTEGER_FEE_RATE_FORMAT.format(feeRate / 1000) + "k";
|
||||
}
|
||||
return feeRate > 0d && feeRate < Transaction.DEFAULT_MIN_RELAY_FEE ? FRACTIONAL_FEE_RATE_FORMAT.format(feeRate) : INTEGER_FEE_RATE_FORMAT.format(feeRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -34,14 +45,100 @@ public class FeeRangeSlider extends Slider {
|
||||
});
|
||||
|
||||
updateTrackHighlight();
|
||||
|
||||
valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue != null) {
|
||||
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) {
|
||||
setValue(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()) {
|
||||
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
setMin(1.0d);
|
||||
}
|
||||
setMax(AppServices.getLongFeeRatesRange().size() - 1);
|
||||
updateTrackHighlight();
|
||||
} else if(value == getMin() && isLongFeeRange()) {
|
||||
if(AppServices.getMinimumRelayFeeRate() < Transaction.DEFAULT_MIN_RELAY_FEE) {
|
||||
setMin(0.0d);
|
||||
}
|
||||
setMax(AppServices.getFeeRatesRange().size() - 1);
|
||||
updateTrackHighlight();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isLongFeeRange() {
|
||||
return getMax() > AppServices.getFeeRatesRange().size() - 1;
|
||||
}
|
||||
|
||||
public void updateTrackHighlight() {
|
||||
@@ -75,7 +172,7 @@ public class FeeRangeSlider extends Slider {
|
||||
private Map<Integer, Double> getTargetBlocksFeeRates() {
|
||||
Map<Integer, Double> retrievedFeeRates = AppServices.getTargetBlockFeeRates();
|
||||
if(retrievedFeeRates == null) {
|
||||
retrievedFeeRates = TARGET_BLOCKS_RANGE.stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> FALLBACK_FEE_RATE,
|
||||
retrievedFeeRates = TARGET_BLOCKS_RANGE.stream().collect(Collectors.toMap(java.util.function.Function.identity(), v -> getFallbackFeeRate(),
|
||||
(u, v) -> { throw new IllegalStateException("Duplicate target blocks"); },
|
||||
LinkedHashMap::new));
|
||||
}
|
||||
@@ -96,7 +193,10 @@ 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)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")
|
||||
);
|
||||
@@ -203,7 +203,9 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
for(Wallet wallet : wallets) {
|
||||
if(scriptType.equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
keystore.setLabel(importer.getName().replace(" Multisig", ""));
|
||||
if(Keystore.DEFAULT_LABEL.equals(keystore.getLabel())) {
|
||||
keystore.setLabel(importer.getName().replace(" Multisig", ""));
|
||||
}
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(importer.getWalletModel());
|
||||
return keystore;
|
||||
@@ -240,6 +242,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,14 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreExportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
@@ -35,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();
|
||||
@@ -153,7 +155,9 @@ public class FileKeystoreExportPane extends TitledDescriptionPane {
|
||||
} else {
|
||||
QRDisplayDialog qrDisplayDialog;
|
||||
if(exporter instanceof Bip129) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), baos.toByteArray(), false);
|
||||
UR ur = UR.fromBytes(baos.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, baos.toByteArray());
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
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.RegistryItem;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -10,6 +14,10 @@ import com.sparrowwallet.sparrow.event.TimedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletExportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Control;
|
||||
@@ -24,6 +32,8 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.sparrowwallet.sparrow.wallet.SettingsController.getUROutputDescriptor;
|
||||
|
||||
public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
private final Wallet wallet;
|
||||
private final WalletExport exporter;
|
||||
@@ -31,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();
|
||||
@@ -115,16 +125,12 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
final String walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
|
||||
String walletPassword = password.get().asString();
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(copy, password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
|
||||
try {
|
||||
exportWallet(file, decryptedWallet);
|
||||
} finally {
|
||||
decryptedWallet.clearPrivate();
|
||||
}
|
||||
exportWallet(file, decryptedWallet, walletPassword);
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
@@ -134,25 +140,49 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
decryptWalletService.start();
|
||||
}
|
||||
} else {
|
||||
exportWallet(file, wallet);
|
||||
exportWallet(file, wallet, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void exportWallet(File file, Wallet exportWallet) {
|
||||
private void exportWallet(File file, Wallet exportWallet, String password) {
|
||||
try {
|
||||
if(file != null) {
|
||||
try(OutputStream outputStream = new FileOutputStream(file)) {
|
||||
exporter.exportWallet(exportWallet, outputStream);
|
||||
FileWalletExportService fileWalletExportService = new FileWalletExportService(exporter, file, exportWallet, password);
|
||||
fileWalletExportService.setOnSucceeded(event -> {
|
||||
EventManager.get().post(new WalletExportEvent(exportWallet));
|
||||
}
|
||||
});
|
||||
fileWalletExportService.setOnFailed(event -> {
|
||||
Throwable e = event.getSource().getException();
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Export Error", errorMessage);
|
||||
});
|
||||
fileWalletExportService.start();
|
||||
} else {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
exporter.exportWallet(exportWallet, outputStream);
|
||||
exporter.exportWallet(exportWallet, outputStream, password);
|
||||
QRDisplayDialog qrDisplayDialog;
|
||||
if(exporter instanceof CoboVaultMultisig) {
|
||||
qrDisplayDialog = new QRDisplayDialog(RegistryType.BYTES.toString(), outputStream.toByteArray(), true);
|
||||
} else if(exporter instanceof PassportMultisig || exporter instanceof KeystoneMultisig || exporter instanceof JadeMultisig || exporter instanceof Bip129) {
|
||||
} 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 || exporter instanceof WalletLabels) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
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);
|
||||
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));
|
||||
}
|
||||
@@ -165,6 +195,42 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Export Error", errorMessage);
|
||||
} finally {
|
||||
if(file == null && password != null) {
|
||||
exportWallet.clearPrivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileWalletExportService extends Service<Void> {
|
||||
private final WalletExport exporter;
|
||||
private final File file;
|
||||
private final Wallet wallet;
|
||||
private final String password;
|
||||
|
||||
public FileWalletExportService(WalletExport exporter, File file, Wallet wallet, String password) {
|
||||
this.exporter = exporter;
|
||||
this.file = file;
|
||||
this.wallet = wallet;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
try(OutputStream outputStream = new FileOutputStream(file)) {
|
||||
exporter.exportWallet(wallet, outputStream, password);
|
||||
} finally {
|
||||
if(password != null) {
|
||||
wallet.clearPrivate();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@ import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.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 +31,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 +40,36 @@ 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));
|
||||
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");
|
||||
}
|
||||
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);
|
||||
importScannedWallet(wallet);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
@@ -69,58 +79,75 @@ 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.setName(importer.getName());
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), null));
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
Wallet wallet = wallets.stream().filter(w -> w.getPolicyType() == policyType && w.getScriptType() == scriptType).findFirst().orElseThrow(ImportException::new);
|
||||
importScannedWallet(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 void importScannedWallet(Wallet wallet) {
|
||||
wallet.setName(importer.getName());
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), null));
|
||||
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
if(keystore.getSource() == KeystoreSource.SW_WATCH) {
|
||||
if(Keystore.DEFAULT_LABEL.equals(keystore.getLabel())) {
|
||||
keystore.setLabel(importer.getName());
|
||||
}
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(importer.getWalletModel());
|
||||
}
|
||||
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
}
|
||||
|
||||
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 +157,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 +178,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);
|
||||
@@ -472,18 +508,93 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(qrText, true);
|
||||
qrDisplayDialog.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<ButtonType> optButtonType = qrDisplayDialog.showAndWait();
|
||||
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.NEXT_FORWARD) {
|
||||
if(optButtonType.isPresent() && optButtonType.get().getButtonData() == ButtonBar.ButtonData.OK_DONE) {
|
||||
scanQr();
|
||||
}
|
||||
}
|
||||
|
||||
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,174 +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 != MixFailReason.CANCEL) {
|
||||
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().substring(0, 1).toUpperCase(Locale.ROOT) + mixProgress.getMixStep().getMessage().substring(1);
|
||||
tt.setText(status);
|
||||
setTooltip(tt);
|
||||
|
||||
if(mixProgress.getMixStep() == MixStep.REGISTERED_INPUT) {
|
||||
tt.setOnShowing(event -> {
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
Whirlpool.RegisteredInputsService registeredInputsService = new Whirlpool.RegisteredInputsService(whirlpool, mixProgress.getPoolId());
|
||||
registeredInputsService.setOnSucceeded(eventStateHandler -> {
|
||||
if(registeredInputsService.getValue() != null) {
|
||||
tt.setText(status + " (1 of " + registeredInputsService.getValue() + ")");
|
||||
}
|
||||
});
|
||||
registeredInputsService.start();
|
||||
});
|
||||
}
|
||||
} 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);
|
||||
@@ -139,6 +139,7 @@ public class MnemonicGridDialog extends Dialog<List<String>> {
|
||||
|
||||
dialogPane.setPrefWidth(952);
|
||||
dialogPane.setPrefHeight(500);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
@@ -255,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();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user